diff --git a/.eslintrc-gjs.yml b/.eslintrc-gjs.yml new file mode 100644 index 00000000..6e475f5c --- /dev/null +++ b/.eslintrc-gjs.yml @@ -0,0 +1,368 @@ +# Try our best to adhere to the guidelines: +# https://gjs.guide/extensions/review-guidelines/review-guidelines.html#code-must-not-be-obfuscated + +# Contains some additional styling rules to automatically fix generated javascript: +# * At the 'padding-line-between-statements:' rule: +# https://eslint.org/docs/latest/rules/padding-line-between-statements +# * 'sourceType: "module"' as additional 'parserOptions:' + +# Official GJS styling: https://gitlab.gnome.org/GNOME/gjs/-/blob/fb11886617fe6b332427cdb5b23b740fc4193d2c/.eslintrc.yml +--- +# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +# SPDX-FileCopyrightText: 2018 Claudio André + +env: + es2021: true +extends: 'eslint:recommended' +plugins: + - jsdoc +rules: + array-bracket-newline: + - error + - consistent + array-bracket-spacing: + - error + - never + array-callback-return: error + arrow-parens: + - error + - as-needed + arrow-spacing: error + block-scoped-var: error + block-spacing: error + brace-style: error + # Waiting for this to have matured a bit in eslint + # camelcase: + # - error + # - properties: never + # allow: [^vfunc_, ^on_, _instance_init] + comma-dangle: + - error + - arrays: always-multiline + objects: always-multiline + functions: never + comma-spacing: + - error + - before: false + after: true + comma-style: + - error + - last + computed-property-spacing: error + curly: + - error + - multi-or-nest + - consistent + dot-location: + - error + - property + eol-last: error + eqeqeq: error + func-call-spacing: error + func-name-matching: error + func-style: + - error + - declaration + - allowArrowFunctions: true + indent: + - error + - 4 + - ignoredNodes: + # Allow not indenting the body of GObject.registerClass, since in the + # future it's intended to be a decorator + - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' + # Allow dedenting chained member expressions + MemberExpression: 'off' + jsdoc/check-alignment: error + jsdoc/check-param-names: error + jsdoc/check-tag-names: error + jsdoc/check-types: error + jsdoc/implements-on-classes: error + jsdoc/require-jsdoc: error + jsdoc/require-param: error + jsdoc/require-param-description: error + jsdoc/require-param-name: error + jsdoc/require-param-type: error + jsdoc/tag-lines: + - error + - never + - startLines: 1 + key-spacing: + - error + - beforeColon: false + afterColon: true + keyword-spacing: + - error + - before: true + after: true + linebreak-style: + - error + - unix + # Each class member has a forced doc which is a block comment. + # Block comment spacing is forced too, disabling this rule. + # lines-between-class-members: + # - error + # - always + # - exceptAfterSingleLine: true + max-nested-callbacks: error + max-statements-per-line: error + new-parens: error + no-array-constructor: error + no-await-in-loop: error + no-caller: error + no-constant-condition: + - error + - checkLoops: false + no-div-regex: error + no-empty: + - error + - allowEmptyCatch: true + no-extra-bind: error + no-extra-parens: + - error + - all + - conditionalAssign: false + nestedBinaryExpressions: false + returnAssign: false + no-implicit-coercion: + - error + - allow: + - '!!' + no-invalid-this: error + no-iterator: error + no-label-var: error + no-lonely-if: error + no-loop-func: error + no-nested-ternary: error + no-new-object: error + no-new-wrappers: error + no-octal-escape: error + no-proto: error + no-prototype-builtins: 'off' + no-restricted-globals: [error, window] + no-restricted-properties: + - error + - object: imports + property: format + message: Use template strings + - object: pkg + property: initFormat + message: Use template strings + - object: Lang + property: copyProperties + message: Use Object.assign() + - object: Lang + property: bind + message: Use arrow notation or Function.prototype.bind() + - object: Lang + property: Class + message: Use ES6 classes + no-restricted-syntax: + - error + - selector: >- + MethodDefinition[key.name="_init"] > + FunctionExpression[params.length=1] > + BlockStatement[body.length=1] + CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] > + Identifier:first-child + message: _init() that only calls super._init() is unnecessary + - selector: >- + MethodDefinition[key.name="_init"] > + FunctionExpression[params.length=0] > + BlockStatement[body.length=1] + CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"] + message: _init() that only calls super._init() is unnecessary + - selector: BinaryExpression[operator="instanceof"][right.name="Array"] + message: Use Array.isArray() + no-return-assign: error + no-return-await: error + no-self-compare: error + no-shadow: error + no-shadow-restricted-names: error + no-spaced-func: error + no-tabs: error + no-template-curly-in-string: error + no-throw-literal: error + no-trailing-spaces: error + no-undef-init: error + no-unneeded-ternary: error + no-unused-expressions: error + no-unused-vars: + - error + # Vars use a suffix _ instead of a prefix because of file-scope private vars + - varsIgnorePattern: (^unused|_$) + argsIgnorePattern: ^(unused|_) + no-useless-call: error + no-useless-computed-key: error + no-useless-concat: error + no-useless-constructor: error + no-useless-rename: error + no-useless-return: error + no-whitespace-before-property: error + no-with: error + nonblock-statement-body-position: + - error + - below + object-curly-newline: + - error + - consistent: true + multiline: true + object-curly-spacing: error + object-shorthand: error + operator-assignment: error + operator-linebreak: error + padded-blocks: + - error + - never + # These may be a bit controversial, we can try them out and enable them later + # prefer-const: error + # prefer-destructuring: error + prefer-numeric-literals: error + prefer-promise-reject-errors: error + prefer-rest-params: off # Generated code triggered this, turn off for now + prefer-spread: error + prefer-template: error + quotes: + - error + - single + - avoidEscape: true + require-await: error + rest-spread-spacing: error + semi: + - error + - always + semi-spacing: + - error + - before: false + after: true + semi-style: error + space-before-blocks: error + space-before-function-paren: + - error + - named: never + # for `function ()` and `async () =>`, preserve space around keywords + anonymous: always + asyncArrow: always + space-in-parens: error + space-infix-ops: + - error + - int32Hint: false + space-unary-ops: error + spaced-comment: error + switch-colon-spacing: error + symbol-description: error + template-curly-spacing: error + template-tag-spacing: error + unicode-bom: error + wrap-iife: + - error + - inside + yield-star-spacing: error + yoda: error + padding-line-between-statements: + - error + - blankLine: always + prev: class + next: function + - blankLine: always + prev: function + next: class + - blankLine: always + prev: import + next: function + - blankLine: always + prev: import + next: var + - blankLine: always + prev: import + next: let + - blankLine: always + prev: import + next: const + - blankLine: always + prev: const + next: import + - blankLine: always + prev: const + next: let + - blankLine: always + prev: let + next: const + - blankLine: always + prev: const + next: function + - blankLine: always + prev: let + next: function + - blankLine: always + prev: class + next: export + - blankLine: always + prev: function + next: export + - blankLine: always + prev: '*' + next: return + - blankLine: always + prev: '*' + next: if + - blankLine: always + prev: if + next: '*' + - blankLine: always + prev: '*' + next: try + - blankLine: always + prev: try + next: '*' + - blankLine: always + prev: '*' + next: switch + - blankLine: always + prev: switch + next: '*' + - blankLine: always + prev: '*' + next: while + - blankLine: always + prev: while + next: '*' + - blankLine: always + prev: '*' + next: do + - blankLine: always + prev: '*' + next: for + - blankLine: always + prev: for + next: '*' + lines-around-comment: + - error + - allowClassStart: true + allowBlockStart: true + beforeLineComment: true +settings: + jsdoc: + mode: typescript +globals: + ARGV: readonly + Debugger: readonly + GIRepositoryGType: readonly + globalThis: readonly + imports: readonly + Intl: readonly + log: readonly + logError: readonly + print: readonly + printerr: readonly + window: readonly + TextEncoder: readonly + TextDecoder: readonly + console: readonly + setTimeout: readonly + setInterval: readonly + clearTimeout: readonly + clearInterval: readonly +parserOptions: + ecmaVersion: 2022 + sourceType: "module" diff --git a/.eslintrc.yml b/.eslintrc.yml new file mode 100644 index 00000000..f7487a8e --- /dev/null +++ b/.eslintrc.yml @@ -0,0 +1,291 @@ +--- +# SPDX-License-Identifier: MIT OR LGPL-2.0-or-later +# SPDX-FileCopyrightText: 2018 Claudio André +env: + es2021: true +extends: + - 'eslint:recommended' + - 'plugin:@typescript-eslint/recommended-requiring-type-checking' + - 'plugin:jsdoc/recommended-typescript-error' +parser: "@typescript-eslint/parser" +plugins: + - jsdoc + - "@typescript-eslint" +rules: + array-bracket-newline: + - error + - consistent + array-bracket-spacing: + - error + - never + array-callback-return: error + arrow-parens: + - error + - as-needed + arrow-spacing: error + block-scoped-var: error + block-spacing: error + brace-style: error + # Waiting for this to have matured a bit in eslint + # camelcase: + # - error + # - properties: never + # allow: [^vfunc_, ^on_, _instance_init] + comma-dangle: + - error + - arrays: always-multiline + objects: always-multiline + functions: never + comma-spacing: + - error + - before: false + after: true + comma-style: + - error + - last + computed-property-spacing: error + curly: + - error + - multi-or-nest + - consistent + dot-location: + - error + - property + eol-last: error + eqeqeq: error + "@typescript-eslint/explicit-function-return-type": error + func-call-spacing: error + func-name-matching: error + func-style: + - error + - declaration + - allowArrowFunctions: true + indent: + - error + - 4 + - ignoredNodes: + # Allow not indenting the body of GObject.registerClass, since in the + # future it's intended to be a decorator + - 'CallExpression[callee.object.name=GObject][callee.property.name=registerClass] > ClassExpression:first-child' + # Allow de denting chained member expressions + MemberExpression: 'off' + jsdoc/check-alignment: error + jsdoc/check-param-names: error + jsdoc/check-tag-names: error + jsdoc/check-types: error + jsdoc/implements-on-classes: error + jsdoc/no-types: off + jsdoc/require-description: error + jsdoc/require-jsdoc: + - error + - require: + ClassDeclaration: true + MethodDefinition: true + jsdoc/require-param: error + jsdoc/require-param-description: error + jsdoc/require-param-name: error + jsdoc/require-param-type: error + jsdoc/tag-lines: + - error + - never + - startLines: 1 + key-spacing: + - error + - beforeColon: false + afterColon: true + keyword-spacing: + - error + - before: true + after: true + linebreak-style: + - error + - unix + lines-between-class-members: + - error + - always + - exceptAfterSingleLine: true + max-nested-callbacks: error + max-statements-per-line: error + new-parens: error + no-array-constructor: error + no-await-in-loop: error + no-caller: error + no-constant-condition: + - error + - checkLoops: false + no-div-regex: error + no-empty: + - error + - allowEmptyCatch: true + no-extra-bind: error + no-extra-parens: off + '@typescript-eslint/no-extra-parens': + - error + - all + - conditionalAssign: false + nestedBinaryExpressions: false + returnAssign: false + no-implicit-coercion: + - error + - allow: + - '!!' + no-invalid-this: off + "@typescript-eslint/no-invalid-this": "error" + no-iterator: error + no-label-var: error + no-lonely-if: error + no-loop-func: error + no-nested-ternary: error + no-new-object: error + no-new-wrappers: error + no-octal-escape: error + no-proto: error + no-prototype-builtins: 'off' + no-restricted-globals: [error, window] + no-restricted-properties: + - error + - object: imports + property: format + message: Use template strings + - object: pkg + property: initFormat + message: Use template strings + - object: Lang + property: copyProperties + message: Use Object.assign() + - object: Lang + property: bind + message: Use arrow notation or Function.prototype.bind() + - object: Lang + property: Class + message: Use ES6 classes + no-restricted-syntax: + - error + - selector: >- + MethodDefinition[key.name="_init"] > + FunctionExpression[params.length=1] > + BlockStatement[body.length=1] + CallExpression[arguments.length=1][callee.object.type="Super"][callee.property.name="_init"] > + Identifier:first-child + message: _init() that only calls super._init() is unnecessary + - selector: >- + MethodDefinition[key.name="_init"] > + FunctionExpression[params.length=0] > + BlockStatement[body.length=1] + CallExpression[arguments.length=0][callee.object.type="Super"][callee.property.name="_init"] + message: _init() that only calls super._init() is unnecessary + - selector: BinaryExpression[operator="instanceof"][right.name="Array"] + message: Use Array.isArray() + no-return-assign: error + no-return-await: error + no-self-compare: error + no-shadow: off + "@typescript-eslint/no-shadow": error + no-shadow-restricted-names: error + no-spaced-func: error + no-tabs: error + no-template-curly-in-string: error + no-throw-literal: error + no-trailing-spaces: error + no-undef-init: error + no-unneeded-ternary: error + no-unused-expressions: error + no-unused-vars: off + "@typescript-eslint/no-unused-vars": + - error + # Vars use a suffix _ instead of a prefix because of file-scope private vars + - varsIgnorePattern: (^unused|_$) + argsIgnorePattern: ^(unused|_) + no-useless-call: error + no-useless-computed-key: error + no-useless-concat: error + no-useless-constructor: error + no-useless-rename: error + no-useless-return: error + no-whitespace-before-property: error + no-with: error + nonblock-statement-body-position: + - error + - below + object-curly-newline: + - error + - consistent: true + multiline: true + object-curly-spacing: error + object-shorthand: error + operator-assignment: error + operator-linebreak: error + padded-blocks: + - error + - never + # These may be a bit controversial, we can try them out and enable them later + # prefer-const: error + # prefer-destructuring: error + prefer-numeric-literals: error + prefer-promise-reject-errors: error + prefer-rest-params: error + prefer-spread: error + prefer-template: error + quotes: + - error + - single + - avoidEscape: true + require-await: error + rest-spread-spacing: error + semi: + - error + - always + semi-spacing: + - error + - before: false + after: true + semi-style: error + space-before-blocks: error + space-before-function-paren: + - error + - named: never + # for `function ()` and `async () =>`, preserve space around keywords + anonymous: always + asyncArrow: always + space-in-parens: error + space-infix-ops: + - error + - int32Hint: false + space-unary-ops: error + spaced-comment: error + switch-colon-spacing: error + symbol-description: error + template-curly-spacing: error + template-tag-spacing: error + unicode-bom: error + wrap-iife: + - error + - inside + yield-star-spacing: error + yoda: error +settings: + jsdoc: + mode: typescript +globals: + ARGV: readonly + Debugger: readonly + GIRepositoryGType: readonly + globalThis: readonly + imports: readonly + Intl: readonly + log: readonly + logError: readonly + print: readonly + printerr: readonly + window: readonly + TextEncoder: readonly + TextDecoder: readonly + console: readonly + setTimeout: readonly + setInterval: readonly + clearTimeout: readonly + clearInterval: readonly +parserOptions: + ecmaVersion: 2022 + sourceType: "module" + project: ['./tsconfig.json'] diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..96b28a8f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,55 @@ +name: Build and create ZIP +run-name: Generate javascript, ui and schema to publish +on: [push, pull_request] +jobs: + check: + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: | + sudo apt -q update + sudo apt -q install --no-install-recommends npm + - name: Check out repository code + uses: actions/checkout@v3 + - name: Setup environment + run: | + ${{github.workspace}}/build.sh setup_env + - name: Check TypeScript + run: | + ${{github.workspace}}/build.sh check + build: + runs-on: ubuntu-latest + steps: + - name: Install dependencies + run: | + sudo apt -q update + sudo apt -q install --no-install-recommends npm libglib2.0-0 bash gnome-shell libgtk-4-bin libgtk-4-common libgtk-4-dev libadwaita-1-dev gir1.2-adw-1 gir1.2-gtk-4.0 zstd + # TODO: Remove the next step once "Jammy" (22.04) receives version 0.8+ and add to the install list above + # https://launchpad.net/ubuntu/+source/blueprint-compiler + - name: Build recent blueprint-compiler + run: | + sudo apt -q install --no-install-recommends meson ninja-build + git clone https://gitlab.gnome.org/jwestman/blueprint-compiler.git + cd blueprint-compiler + meson _build + sudo ninja -C _build install + - name: Check out repository code + uses: actions/checkout@v3 + # TODO: Remove the next step once "Jammy" (22.04) receives version 1.2+ + # https://launchpad.net/ubuntu/+source/libadwaita-1 + - name: Update to recent libadwaita + run: | + mkdir libadwaita + cd libadwaita || exit 1 + wget https://launchpad.net/ubuntu/+source/libadwaita-1/1.2.0-1ubuntu2/+build/24480571/+files/gir1.2-adw-1_1.2.0-1ubuntu2_amd64.deb + wget https://launchpad.net/ubuntu/+source/libadwaita-1/1.2.0-1ubuntu2/+build/24480571/+files/libadwaita-1-0_1.2.0-1ubuntu2_amd64.deb + wget https://launchpad.net/ubuntu/+source/libadwaita-1/1.2.0-1ubuntu2/+build/24480571/+files/libadwaita-1-dev_1.2.0-1ubuntu2_amd64.deb + sudo dpkg --recursive --install . + - run: ${{github.workspace}}/build.sh setup_env + - run: ${{github.workspace}}/build.sh build + - run: ${{github.workspace}}/build.sh format + - run: ${{github.workspace}}/build.sh copy_static + - uses: actions/upload-artifact@v3 + with: + name: randomwallpaper@iflow.space.shell-extension.zip + path: ${{github.workspace}}/randomwallpaper@iflow.space diff --git a/.gitignore b/.gitignore index f5e78c9c..abdc8f0f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,11 @@ .idea -# ready to deploy to gnome extensions -random-wallpaper-gnome3.zip -randomwallpaper@iflow.space.zip # Temporary ui files **/*~ + +# Generated stuff +randomwallpaper@iflow.space/ +randomwallpaper@iflow.space.shell-extension.zip + +# Build stuff +node_modules/ diff --git a/README.md b/README.md index 9c97000e..a7011610 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ Install and try the extension at [extensions.gnome.org](https://extensions.gnome * Automatic renewal (Auto-Fetching) ## Installation (symlink to the repository) -Requires [`blueprint-compiler`](https://repology.org/project/blueprint-compiler/versions) at install and update time. +Requires [`blueprint-compiler`](https://repology.org/project/blueprint-compiler/versions) and [`npm`](https://repology.org/project/npm/versions) at install and update time. Clone the repository and run `./build.sh && ./install.sh` in the repository folder to make a symbolic link from the extensions folder to the git repository. This installation will depend on the repository folder, so do not delete the cloned folder. @@ -38,9 +38,9 @@ __Installing this way has various advantages:__ * Updating the extension with `git pull && ./build.sh` ## Installation (manually) -Requires [`blueprint-compiler`](https://repology.org/project/blueprint-compiler/versions) at install and update time. +Requires [`blueprint-compiler`](https://repology.org/project/blueprint-compiler/versions) and [`npm`](https://repology.org/project/npm/versions) at install and update time. -Clone or download the repository and copy the folder `randomwallpaper@iflow.space` in the repository to `~/.local/share/gnome-shell/extensions/`. +Clone or download the repository and copy the folder `randomwallpaper@iflow.space` in the repository to `$XDG_DATA_HOME/gnome-shell/extensions/` (usually `$HOME/.local/share/gnome-shell/extensions/`). Run `./build.sh` inside the repository. Then open the command prompt (Alt+F2) end enter `r` to restart the gnome session. @@ -50,27 +50,39 @@ Now you should be able to activate the extension through the gnome-tweak-tool. ## Uninstall Run `./install uninstall` to delete the symbolic link. -If you installed the extension manually you have to delete the extension folder `randomwallpaper@iflow.space` in `~/.local/share/gnome-shell/extensions/`. +If you installed the extension manually you have to delete the extension folder `randomwallpaper@iflow.space` in `$XDG_DATA_HOME/gnome-shell/extensions/` (usually `$HOME/.local/share/gnome-shell/extensions/`). ## Debugging You can follow the output of the extension with `./debug.sh`. Information should be printed using the existing logger class but can also be printed with `global.log()` (not recommended). To debug the `prefs.js` use `./debug.sh prefs`. -## Compiling schemas -This can be done with the command: `glib-compile-schemas randomwallpaper@iflow.space/schemas/` - -## Compiling UI -Requires [`blueprint-compiler`](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/). -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/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/mySource.js` -1. Add the new source to `…/ui/sourceRow.js` -1. Create a adapter to read the settings and fetching the images and additional information in `…/adapter/mySource.js` by extending the `BaseAdapter`. - * Add your adapter to `…/wallpaperController.js` +## Compiling individual parts +### Schemas +This can be done with the command: +~~~ +glib-compile-schemas --targetdir="randomwallpaper@iflow.space/schemas/" "src/schemas" +~~~ + +### UI +Requires [`blueprint-compiler`](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/): +~~~ +blueprint-compiler batch-compile "src/ui" "randomwallpaper@iflow.space/ui" "src"/ui/*.blp +~~~ + +### TypeScript +Requires [`npm`](https://repology.org/project/npm/versions): +~~~ +npm install +npx --silent tsc +~~~ + +## Adding new sources +1. Build UI for settings using the [blueprint-compiler](https://jwestman.pages.gitlab.gnome.org/blueprint-compiler/) language in `src/ui/mySource.blp` - see [Workbench](https://apps.gnome.org/app/re.sonny.Workbench/) for a live preview editor. +1. Create and add a settings layout to the `src/schemas/….gschema.xml`. Also add your source to the `types` enum. +1. Create your logic hooking the settings in a `src/ui/mySource.ts` +1. Add the new source to `src/ui/sourceRow.ts:_getSettingsGroup()`, don't forget the import statement. +1. Create a adapter to read the settings and fetching the images and additional information in `src/adapter/mySource.ts` by extending the `BaseAdapter`. +1. Add your adapter to `src/wallpaperController.ts:_getRandomAdapter()`, don't forget the import statement. ## Support Me If you enjoy this extension and want to support the development, then feel free to buy me a coffee. :wink: :coffee: diff --git a/build.sh b/build.sh index fc84dcf6..9ea30c86 100755 --- a/build.sh +++ b/build.sh @@ -1,25 +1,142 @@ -#!/bin/bash - -BASEDIR="randomwallpaper@iflow.space" -ZIPNAME="$BASEDIR.zip" - -rm "$ZIPNAME" -rm "$BASEDIR/schemas/gschemas.compiled" -rm "$BASEDIR/wallpapers/"* -glib-compile-schemas "$BASEDIR/schemas/" - -# cd "$BASEDIR/ui" || exit 1 -blueprint-compiler batch-compile "$BASEDIR/ui" "$BASEDIR/ui" \ - "$BASEDIR/ui/genericJson.blp" \ - "$BASEDIR/ui/localFolder.blp" \ - "$BASEDIR/ui/pageGeneral.blp" \ - "$BASEDIR/ui/pageSources.blp" \ - "$BASEDIR/ui/reddit.blp" \ - "$BASEDIR/ui/sourceRow.blp" \ - "$BASEDIR/ui/unsplash.blp" \ - "$BASEDIR/ui/urlSource.blp" \ - "$BASEDIR/ui/wallhaven.blp" - -cd "$BASEDIR" || exit 1 -zip -r "$ZIPNAME" . -mv "$ZIPNAME" .. +#!/usr/bin/env bash + +UUID="randomwallpaper@iflow.space" + +# fail on error +set -e + +# https://unix.stackexchange.com/a/20325 +if [[ $EUID -eq 0 ]]; then + echo "This script must NOT be run as root" 1>&2 + exit 1 +fi + +# https://stackoverflow.com/a/246128 +SCRIPTDIR=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd) + +SRCDIR="$SCRIPTDIR/src" +DESTDIR="$SCRIPTDIR/$UUID" + +cd "$SCRIPTDIR" || exit 1 + +check_command() { + if ! command -v "$1" &>/dev/null; then + echo "Please install \"$1\" and make sure it's available in your \$PATH" + exit 1 + fi +} + +setup_environment() { + check_command "npm" + + # install, config in package.json + npm --silent install + + # Delete output directory, everything will be rewritten + rm -r "$DESTDIR" &>/dev/null || true +} + +compile_ui() { + check_command "blueprint-compiler" + + # Compile UI files + blueprint-compiler batch-compile "$DESTDIR/ui" "$SRCDIR/ui" "$SRCDIR"/ui/*.blp +} + +compile_js() { + check_command "npm" + + # TypeScript to JavaScript, config in tsconfig.json + npx --silent tsc + + # extension.js and prefs.js can't be modules (yet) while dynamically loaded by GJS… + # https://gjs.guide/extensions/overview/imports-and-modules.html#imports-and-modules + # …and TypeScript can't compile specific files to "not a module" in overall module mode. + # https://github.com/microsoft/TypeScript/issues/41567 + sed -i -E "s#export \{\};##g" "$DESTDIR/extension.js" + sed -i -E "s#export \{\};##g" "$DESTDIR/prefs.js" +} + +# TODO: Drop compiled schemas when only targeting Gnome 44+ +# https://gjs.guide/extensions/upgrading/gnome-shell-44.html#gsettings-schema +compile_schemas() { + check_command "glib-compile-schemas" + mkdir -p "$DESTDIR/schemas/" + + # the pack command also compiles the schemas but only into the zip file + glib-compile-schemas --targetdir="$DESTDIR/schemas" "$SRCDIR/schemas/" +} + +format_js() { + check_command "npm" + + # Circumvent not found typescript rules that might be mentioned in code comments but will give an error + # when only checking with javascript rules + # https://stackoverflow.com/questions/64614131/how-can-i-disable-definition-for-rule-custom-rule-was-not-found-errors + shopt -s globstar nullglob + for file in "$DESTDIR"/**/*.js; do + sed -i -E "s#@typescript-eslint/await-thenable##g" "$file" + sed -i -E "s#@typescript-eslint/no-unused-vars##g" "$file" + sed -i -E "s#@typescript-eslint/no-unsafe-argument##g" "$file" + sed -i -E "s#@typescript-eslint/no-unsafe-member-access##g" "$file" + sed -i -E "s#@typescript-eslint/no-unsafe-call##g" "$file" + sed -i -E "s#@typescript-eslint/ban-ts-comment##g" "$file" + sed -i -E "s#@typescript-eslint/no-unsafe-enum-comparison##g" "$file" + done + + # Format js using the official gjs stylesheet and a few manual quirks + npx --silent eslint --no-eslintrc --config "$SCRIPTDIR/.eslintrc-gjs.yml" --fix "$DESTDIR/**/*.js" +} + +check_ts() { + check_command "npm" + + npx --silent eslint "$SRCDIR/**/*.ts" +} + +copy_static_files() { + # Copy non generated files to destdir + mkdir -p "$DESTDIR/schemas/" + cp "$SRCDIR/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml" "$DESTDIR/schemas/" + cp "$SRCDIR/metadata.json" "$DESTDIR/" + cp "$SRCDIR/stylesheet.css" "$DESTDIR/" +} + +pack() { + check_command "gnome-extensions" + + # pack everything into a sharable zip file + extra_source=() + for file in "$DESTDIR"/*; do + extra_source+=("--extra-source=$file") + done + + gnome-extensions pack --force "${extra_source[@]}" "$DESTDIR" +} + +if [ $# -eq 0 ]; then + # No arguments, do everything + setup_environment + compile_ui + compile_js + compile_schemas + format_js + check_ts + copy_static_files + pack +elif [ "$1" == "check" ]; then + check_ts +elif [ "$1" == "build" ]; then + compile_ui + compile_js + compile_schemas +elif [ "$1" == "format" ]; then + format_js +elif [ "$1" == "pack" ]; then + copy_static_files + pack +elif [ "$1" == "setup_env" ]; then + setup_environment +elif [ "$1" == "copy_static" ]; then + copy_static_files +fi diff --git a/debug.sh b/debug.sh index 7576081e..dfc15a1c 100755 --- a/debug.sh +++ b/debug.sh @@ -3,5 +3,6 @@ if [ "$1" = "prefs" ]; then journalctl -f /usr/bin/gjs else - journalctl -f /usr/bin/gnome-shell + # https://gjs.guide/extensions/development/debugging.html#logging + journalctl -f GNOME_SHELL_EXTENSION_UUID=randomwallpaper@iflow.space fi diff --git a/install.sh b/install.sh index 8c0599e6..f02bd440 100755 --- a/install.sh +++ b/install.sh @@ -1,6 +1,6 @@ #!/bin/bash -datahome="${XDG_DATA_HOME:-HOME/.local/share}" +datahome="${XDG_DATA_HOME:-$HOME/.local/share}" extensionFolder="randomwallpaper@iflow.space" sourcepath="$PWD/$extensionFolder" @@ -11,7 +11,7 @@ if [ "$1" = "uninstall" ]; then rm "$targetpath/$extensionFolder" else echo "# Making extension directory" - mkdir -p $targetpath + mkdir -p "$targetpath" echo "# Linking extension folder" ln -s "$sourcepath" "$targetpath" fi diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..fc005308 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4444 @@ +{ + "name": "RandomWallpaperGnome3", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "devDependencies": { + "@gi-types/adw1": "^1.1.1", + "@gi-types/base-types": "^1.0.0", + "@gi-types/gjs-environment": "^1.1.0", + "@gi-types/gtk4-types": "^1.0.0", + "@gi-types/shell": "^0.1.6", + "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/parser": "^6.3.0", + "eslint": "^8.47.0", + "eslint-plugin-jsdoc": "^46.4.6", + "typescript": "^5.1.6" + } + }, + "node_modules/@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/@es-joy/jsdoccomment": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", + "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "dev": true, + "dependencies": { + "comment-parser": "1.4.0", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", + "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@gi-types/accountsservice1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/accountsservice1/-/accountsservice1-1.0.1.tgz", + "integrity": "sha512-u2X3p54sl3U5Vptx/DmGVjwf3YdkjqcTaiwPIGBmdhTOcdVBLXbIrVtH50dVkwt6nURASnknOFzVuPBg+jEPCg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/adw1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@gi-types/adw1/-/adw1-1.1.1.tgz", + "integrity": "sha512-w+NxplCUf2AeVdSwkicbVrIOB5QJd9BQo1BI3Xq+2IZkgMRePIUG9/eGsrvmzxKU00EiH211KEwqGe/2UOmomQ==", + "dev": true, + "dependencies": { + "@gi-types/gdk4": "^4.0.1", + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/gsk4": "^4.0.1", + "@gi-types/gtk4": "^4.6.1" + } + }, + "node_modules/@gi-types/atk": { + "version": "2.36.7", + "resolved": "https://registry.npmjs.org/@gi-types/atk/-/atk-2.36.7.tgz", + "integrity": "sha512-PWR/EqgBgqehMfDr48fqvy77yGEKLwtqN0qKnKOtWqDm65LbImoAFydlMx5CUeyE/ZNcGDhQGLimQ0Rk6kVivw==", + "dev": true, + "dependencies": { + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/atk1": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@gi-types/atk1/-/atk1-2.36.0.tgz", + "integrity": "sha512-lCuoaJwqFDV+V+sf4mOcciLCJitiUsGNg/W2Q5Y+21FycPZqlsjgXb0pimo7gLdq11d1lEfHffh3AkY80+7SHw==", + "dev": true, + "dependencies": { + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0" + } + }, + "node_modules/@gi-types/atspi2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/atspi2/-/atspi2-2.0.1.tgz", + "integrity": "sha512-REenxB9zE4J++j92cgqTVKBPS/Pdmlj1JJ38c8t174OObS6nIUyi9B563VyehkfbZN75BZ7sGNihnnsLVsbIrQ==", + "dev": true, + "dependencies": { + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/base-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/base-types/-/base-types-1.0.0.tgz", + "integrity": "sha512-A+TMfI4f+RgBx5uM5la4tqRrraP8N9B3SDFFPvYyH3h9VMcy03/Fp4X0LVtHuYdAUHmdRR8Z4Got9lwVd975+g==", + "dev": true, + "dependencies": { + "@gi-types/accountsservice1": "^1.0.0", + "@gi-types/atk1": "*", + "@gi-types/atspi2": "*", + "@gi-types/cairo1": "*", + "@gi-types/dbus1": "*", + "@gi-types/gck1": "*", + "@gi-types/gcr3": "*", + "@gi-types/gdkpixbuf2": "*", + "@gi-types/gdm1": "*", + "@gi-types/geoclue2": "*", + "@gi-types/gio2": "*", + "@gi-types/girepository2": "^1.68.0", + "@gi-types/glib2": "*", + "@gi-types/gmodule2": "*", + "@gi-types/gobject2": "*", + "@gi-types/harfbuzz2": "*", + "@gi-types/ibus1": "*", + "@gi-types/json1": "*", + "@gi-types/malcontent0": "*", + "@gi-types/modemmanager1": "*", + "@gi-types/nm1": "*", + "@gi-types/notify0": "*", + "@gi-types/pango1": "*", + "@gi-types/polkit1": "*", + "@gi-types/polkitagent1": "*", + "@gi-types/rsvg2": "*", + "@gi-types/soup2": "*", + "@gi-types/telepathyglib0": "*", + "@gi-types/telepathylogger0": "*", + "@gi-types/upowerglib1": "*", + "@gi-types/xlib2": "*" + } + }, + "node_modules/@gi-types/cairo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/cairo/-/cairo-1.0.3.tgz", + "integrity": "sha512-IT4JbSYgMj2aJbx7Yy8nzHgga2vX5i5OHR2+vXKc9vg/wrnExHiSbacWbxO7CZdk6YBqpH/7cjOa4bkTmBN8tg==", + "dev": true, + "dependencies": { + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/cairo1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/cairo1/-/cairo1-1.0.1.tgz", + "integrity": "sha512-Q92XKfTWVmtF3LBX9Kx7TkdRjeaZeb8llC01p9WgbkVfFX1d2VO4LrqYgeNkThwmJpscHzDzGF0uv5njsfd4xg==", + "dev": true, + "dependencies": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/cally": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/cally/-/cally-7.0.3.tgz", + "integrity": "sha512-mezisdoGvnewxncjpejrAa2XO0WIYsv+jD+olQu49cSU3BIBI9vmVMd1cmkngTGqombkd/2C9KwFMq1ZvEOhiQ==", + "dev": true, + "dependencies": { + "@gi-types/atk": "^2.36.7", + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/clutter": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/clutter/-/clutter-7.0.6.tgz", + "integrity": "sha512-hLQ5K+wnq40B1VWFVJPtENw82at2SHHE1CN+BCagjO6L9PpAvO9sCV9uUSbl9K5zrzcfgxYdwFrXW5NP6wSKTg==", + "dev": true, + "dependencies": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/graphene": "^1.0.1", + "@gi-types/json": "^1.6.5", + "@gi-types/pango": "^1.0.5" + } + }, + "node_modules/@gi-types/clutterx11": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/clutterx11/-/clutterx11-7.0.2.tgz", + "integrity": "sha512-xYJPu1tszpuEkfc6L28DBm2gsuC3pemfi8ixm41I/RI2iIHuhxGEV/PtyOIy2pSM1ZmdItbjRQWq7k0Jz5oggQ==", + "dev": true, + "dependencies": { + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/cogl": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/cogl/-/cogl-7.0.3.tgz", + "integrity": "sha512-yub8mzFnUvZfcBGCxeTS/mShT9pOWcx/FaVmBU+NCEdUlv6LwA+kaVeDJx9SjVFOIrybsJ06q7f/QedkQOfUQQ==", + "dev": true, + "dependencies": { + "@gi-types/cairo": "^1.0.3", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/graphene": "^1.0.1" + } + }, + "node_modules/@gi-types/coglpango": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/coglpango/-/coglpango-7.0.2.tgz", + "integrity": "sha512-YH6lmMZeo5HU9mhekIKAtj1OCvK2D+l8hzr1NDz5UIxR1K3oAYqu3sMX1EI0dB24pxcn+mqq/J4xpMO50sCnJA==", + "dev": true, + "dependencies": { + "@gi-types/cogl": "^7.0.3", + "@gi-types/gobject": "^2.66.8", + "@gi-types/pango": "^1.0.5" + } + }, + "node_modules/@gi-types/dbus1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/dbus1/-/dbus1-1.0.1.tgz", + "integrity": "sha512-gFM9UrQCQXxpFB5hg2EW8CRCgY5OTmfidBEh085rux/dqFyGR8lSfDIyN0/RZXrD8rggoQ6JteNhbmNxtQkWmw==", + "dev": true, + "dependencies": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/freetype22": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/freetype22/-/freetype22-2.0.1.tgz", + "integrity": "sha512-3p4XvTSn6o1UCGpla/1KhvMIwZkgIyBtGErbnXuAvGzULtlseQjX2IljY4fi2FKtFd9ngRtIIF71inRPUIj+1w==", + "dev": true, + "dependencies": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gck": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/gck/-/gck-1.0.6.tgz", + "integrity": "sha512-yjbVHprwZ6KE3B/JqMFgHJvAgULuSs0obEWMJFeSpXHgke5Vab8a2Gi0pXdwU5AHVD1rVxhNy/8dkRdiIucwoA==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/gck1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gck1/-/gck1-1.0.1.tgz", + "integrity": "sha512-EkLhQvsb/qHBffTJ5j9aK/KX2nE8LTHDbPSMrXtaTWz3yQYuRDqTyXJWVVUxfcXwKliE9GYyqEOAiXlXCamFHQ==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gcr": { + "version": "3.38.8", + "resolved": "https://registry.npmjs.org/@gi-types/gcr/-/gcr-3.38.8.tgz", + "integrity": "sha512-UuIStqiXrcXA9RmQZJ/T0K5WpFt5GtwVgLooSOVMflnn28ltx7gA0OVD/keUOjGqVPtubr102Jvzu+KNSGbDxQ==", + "dev": true, + "dependencies": { + "@gi-types/gck": "^1.0.6", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.9" + } + }, + "node_modules/@gi-types/gcr3": { + "version": "3.41.1", + "resolved": "https://registry.npmjs.org/@gi-types/gcr3/-/gcr3-3.41.1.tgz", + "integrity": "sha512-sZWGbOQlbtwNoggDDBqFxD1lAyGpIEg7TR5RVVrcQfaMwo7yIkRahvXCy7XCoYnWsEmAw46fEdUI5408Ykn8/g==", + "dev": true, + "dependencies": { + "@gi-types/gck1": "^1.0.1", + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gdesktopenums": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/gdesktopenums/-/gdesktopenums-3.0.3.tgz", + "integrity": "sha512-2Ng2uCCZcgASARPcIxapkTFrkM5std3ANUeW1pc4CJ4ZXLYgwSad8IQxJ4wB7hXkhZR9CYXmoA4KBmScjaHfyA==", + "dev": true, + "dependencies": { + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/gdk": { + "version": "3.24.7", + "resolved": "https://registry.npmjs.org/@gi-types/gdk/-/gdk-3.24.7.tgz", + "integrity": "sha512-t50PlPeiqeNNQvXcKuso7/aiy1zm2zNtDgXZgcmtCfjk2u9m4m5gC4/kePzfYtSBqo15FZdv747ESRLC0zNk6Q==", + "dev": true, + "dependencies": { + "@gi-types/cairo": "^1.0.3", + "@gi-types/gdkpixbuf": "^2.0.5", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/pango": "^1.0.5" + } + }, + "node_modules/@gi-types/gdk4": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gdk4/-/gdk4-4.0.1.tgz", + "integrity": "sha512-Msf4bWdw8A0qorTvbvWgCwMno8nUCGLiDcEhNI6iqh6o8IJKchID9a51j6KeqpHkwSPZAcaR2TKM+CVafa4pOw==", + "dev": true, + "dependencies": { + "@gi-types/cairo1": "^1.0.0", + "@gi-types/gdkpixbuf2": "^2.0.0", + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/pango1": "^1.0.0" + } + }, + "node_modules/@gi-types/gdkpixbuf": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@gi-types/gdkpixbuf/-/gdkpixbuf-2.0.5.tgz", + "integrity": "sha512-bLw6CFCbM0SNi30vkbupoVxE7T5hz+V6DQHLa1Sh0yYwm8OTQ8jYofZNTabfjBiTR3Z4dxtN65DkIpJoD+9fuQ==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/gdkpixbuf2": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/gdkpixbuf2/-/gdkpixbuf2-2.0.2.tgz", + "integrity": "sha512-vwopihyKOjJszDPvMj+T2XO6hMWYRmhW/uSrfCeCudMhzmJBrpIzmRZTYfeTgMMDU8cuWydCjacbiD7hSio0tw==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gmodule2": "^2.0.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gdm1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gdm1/-/gdm1-1.0.1.tgz", + "integrity": "sha512-XfrIzC4fI8SxnjjJ16d3PA5FVMOnkyZrEdg4fYtLK4m/GdytrivAgxsousXlnlIEXuDUA4Nss7ycja6TIa70NA==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/geoclue2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/geoclue2/-/geoclue2-2.0.1.tgz", + "integrity": "sha512-7X7URSNs3fdrER2uUdWmU4FaQry7aBaCyCGTNitmAwKigijv1OihI6oDyIx9Y24rmwrz6Im2+tKrg0bugnxJjQ==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gio": { + "version": "2.66.9", + "resolved": "https://registry.npmjs.org/@gi-types/gio/-/gio-2.66.9.tgz", + "integrity": "sha512-hRqJqQSQ4jLjUSiS7XZ7pXvIdu/+ZuFvIGPqrLFXyYa+aTenHiYg/EaLR3iPbAzkvpyBLRZk7Pmgs0quoK+Vtg==", + "dev": true, + "dependencies": { + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/gio2": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/@gi-types/gio2/-/gio2-2.72.1.tgz", + "integrity": "sha512-V3ASxEbq4xBdw/rcROEbNrTqtFtWfwFOcOsXO+48JPIEqZRTJZcf9LX/ATxi9hxaZXogpmOBMsxuyNsd5ZQXyA==", + "dev": true, + "dependencies": { + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/girepository2": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@gi-types/girepository2/-/girepository2-1.68.0.tgz", + "integrity": "sha512-OYTsgMdQ1UWua6Bvx/rSL29qn9CCZ1rMpoBsfvDs5ol+9nNYsxUIl7AvRIojEk/IbWmYh/K9Kv3Mqi3ntB3aTA==", + "dev": true, + "dependencies": { + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0" + } + }, + "node_modules/@gi-types/gjs-environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gi-types/gjs-environment/-/gjs-environment-1.1.0.tgz", + "integrity": "sha512-WXQ1xwuLou5qobQ76LyYxU5bgZYNbkypm3ZLphNWZLuj8C9MbWmb9BJihdq2IAAyH6+HIrrN6ss1syu7IJZUaA==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0" + } + }, + "node_modules/@gi-types/glib": { + "version": "2.66.10", + "resolved": "https://registry.npmjs.org/@gi-types/glib/-/glib-2.66.10.tgz", + "integrity": "sha512-x34Y/xngzSS/BjA38/XpZjAzQqdWdUFtEVKsAGIMi4g+2ZES5QvBLkg/vNHwzSeoY/vUf+E/aZ3+6xQZFUzRjg==", + "dev": true, + "dependencies": { + "@gi-types/gobject": "^2.66.9" + } + }, + "node_modules/@gi-types/glib2": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/@gi-types/glib2/-/glib2-2.72.1.tgz", + "integrity": "sha512-Mfhs1UGV1a0GOLolqA1xC2oVV5cILbJjGh/M9Db3irqWNIWGM0LUFkoG2nRMBpJ0enhIYbqY9CCpGlMpAnyxOw==", + "dev": true, + "dependencies": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gmodule2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gmodule2/-/gmodule2-2.0.1.tgz", + "integrity": "sha512-thIYqYL3ALKBLR/zpjrlXro1m9UTv6eAYyuIdpPtmQvlaggBLoUakYl6mzNmqcb22OIlq/8jJrVx+cjFxdPd/A==", + "dev": true, + "dependencies": { + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/gobject": { + "version": "2.66.9", + "resolved": "https://registry.npmjs.org/@gi-types/gobject/-/gobject-2.66.9.tgz", + "integrity": "sha512-fr6a0W0UfLQiYYuQg4ZY+u4oFgC0QacjffbCBupiVHyC4NVDH2FbWiEVQEb0V52QmSEZTAfRyc/ZgmDzwpUXuw==", + "dev": true, + "dependencies": { + "@gi-types/glib": "^2.66.9" + } + }, + "node_modules/@gi-types/gobject2": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/@gi-types/gobject2/-/gobject2-2.72.1.tgz", + "integrity": "sha512-1UUD2zzwRo7vJLu0J62L1cgB39wjYNs1+6qS0UBhdLhzkOWK+CnVII9CNh21Z4YBs0Oa2d5REeGUiZO4d7a6Zw==", + "dev": true, + "dependencies": { + "@gi-types/glib2": "^2.72.1" + } + }, + "node_modules/@gi-types/graphene": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/graphene/-/graphene-1.0.1.tgz", + "integrity": "sha512-XWzEdAWdvsJH2ieraRhbXqyoFXXG2HP2P8amSpryBsXuWaiessnhsNKgB+pu93cgrW029nnYJMb6AUJodASMrw==", + "dev": true, + "dependencies": { + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/graphene1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/graphene1/-/graphene1-1.0.1.tgz", + "integrity": "sha512-O5cJG/eIi912eF1zcaXrSur25LgMJDU+VhFDnYuO4Latj32eypFJf5Sw3KOZvN2EoctIBxjutbXLOxYYKMBhHA==", + "dev": true, + "dependencies": { + "@gi-types/gobject2": "^2.68.0" + } + }, + "node_modules/@gi-types/gsk4": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gsk4/-/gsk4-4.0.1.tgz", + "integrity": "sha512-/OtGBQwwtyQw7vjkfC4/P2pQf9hrtB89sXhDyKQG5B2UJ/LTAQQJNK7vu4u2W0Y922WK/zqrzimBy38JUoPO5Q==", + "dev": true, + "dependencies": { + "@gi-types/cairo1": "^1.0.0", + "@gi-types/gdk4": "^4.0.1", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/graphene1": "^1.0.1", + "@gi-types/pango1": "^1.0.0" + } + }, + "node_modules/@gi-types/gtk": { + "version": "3.24.9", + "resolved": "https://registry.npmjs.org/@gi-types/gtk/-/gtk-3.24.9.tgz", + "integrity": "sha512-U2LH7hKlIvPAvjVe0eQX8s6MDjYaFKCRpAGmwR3lIoBPIdvYOyy76odGY1suFKrepBa97iW6JMbEcv/0f2D98w==", + "dev": true, + "dependencies": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/gdk": "^3.24.7", + "@gi-types/gdkpixbuf": "^2.0.5", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.9", + "@gi-types/pango": "^1.0.5", + "@gi-types/xlib": "^2.0.0" + } + }, + "node_modules/@gi-types/gtk4": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@gi-types/gtk4/-/gtk4-4.6.1.tgz", + "integrity": "sha512-WtdHV2S/xParqwJX9BJ/OsGTzoEorEhVAP0W2eJpUk5rqfSlzIXHnNvFvs9T5sT43fRZmefVd3Id1EbxsHKeLg==", + "dev": true, + "dependencies": { + "@gi-types/cairo1": "^1.0.0", + "@gi-types/gdk4": "^4.0.1", + "@gi-types/gdkpixbuf2": "^2.0.0", + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/graphene1": "^1.0.1", + "@gi-types/gsk4": "^4.0.1", + "@gi-types/pango1": "^1.50.1" + } + }, + "node_modules/@gi-types/gtk4-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/gtk4-types/-/gtk4-types-1.0.0.tgz", + "integrity": "sha512-Y7SNMhAX9bUBK/HfJbidMePIjsa/7B6WroLR2nZF3afIzqjYgEugmbYWC85516z0+c1/3Z4eXC8V3j1CleXmGQ==", + "dev": true, + "dependencies": { + "@gi-types/gdk4": "*", + "@gi-types/graphene1": "*", + "@gi-types/gsk4": "*", + "@gi-types/gtk4": "*" + } + }, + "node_modules/@gi-types/gvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/gvc/-/gvc-1.0.2.tgz", + "integrity": "sha512-u5QMX4nI6FJSvnc/Se0JObycRlMznTz6BlZ2CM84i9ePhL0K6vplM0UIeTfWDxJiwS+M9mofQnA10uWt8CZRhA==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/harfbuzz": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/harfbuzz/-/harfbuzz-0.0.0.tgz", + "integrity": "sha512-pqhpLBN5aYl2P6yOPo0HCZTLmcTDiynGkEgfEq1yPwY0qhJpuEJYmaVUzwb7RrQrt7absVZP7ClhhO8TYiporw==", + "dev": true, + "dependencies": { + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/harfbuzz2": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@gi-types/harfbuzz2/-/harfbuzz2-4.4.2.tgz", + "integrity": "sha512-jsntc6SthoOdw4VSetTOKFI0BM1ccDISP1JQhnL5NETrVuYGnNAwRZ2oDIujuG0M70bHDv/MWKiZnmJxXNPrKw==", + "dev": true, + "dependencies": { + "@gi-types/freetype22": "^2.0.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/ibus1": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@gi-types/ibus1/-/ibus1-1.5.1.tgz", + "integrity": "sha512-pwPk1H0zlf3u5DQ0g3ENfN4PY9U1mu5N6dRHYocwXsmENs0EEZvYrHa+SfUC2DEpsIvvMoAfm2lwKAVQ7KqNRg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/json": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@gi-types/json/-/json-1.6.6.tgz", + "integrity": "sha512-YtnjubEVEmnGirOlcwNkwD9hftaOKuJv2t0HnNFXcHuBCZpD72Jt5iuUtyFFvF7cduVR/jAXh71fw5U3kMYB8g==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.9" + } + }, + "node_modules/@gi-types/json1": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@gi-types/json1/-/json1-1.6.1.tgz", + "integrity": "sha512-6DpNf3PTYYKbL96U76R/mWZuAXv32cI0ZDalyKAciKeh9pczHjlnNdjq8h6lDKJc5vKxviHW9TsOdtB3NaXuqQ==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/malcontent0": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/malcontent0/-/malcontent0-0.0.1.tgz", + "integrity": "sha512-zB/M/ew8P1ZEqeXChK9b5PAoqq5TrmkTZ3NkZ0R/DPX/xt7aZRNPwdxrFt9enzrlKImjthcctp/tpk48MgGoKg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/meta": { + "version": "3.38.5", + "resolved": "https://registry.npmjs.org/@gi-types/meta/-/meta-3.38.5.tgz", + "integrity": "sha512-EzYGvZu58j/hDZv/zQX4ooL0Pq4TEP2Okhn0JlVmGpFIB65ZWOkCHQqj3LODdpp327+cWMNYkG5WHZXJAmtuZQ==", + "dev": true, + "dependencies": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gdesktopenums": "^3.0.3", + "@gi-types/gdk": "^3.24.7", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/gtk": "^3.24.8", + "@gi-types/json": "^1.6.5", + "@gi-types/pango": "^1.0.5" + } + }, + "node_modules/@gi-types/modemmanager1": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@gi-types/modemmanager1/-/modemmanager1-1.18.1.tgz", + "integrity": "sha512-eQgs3B0S61qtrIIoa9APcmszDGsVEbxZJwlAOak/4ifF4HYrF42fsTDQslzGoAe0aDe9B/FJe2ycR8LgMgquhg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/nm": { + "version": "1.28.9", + "resolved": "https://registry.npmjs.org/@gi-types/nm/-/nm-1.28.9.tgz", + "integrity": "sha512-sZd0HWWJ4aeXTrbnmTlN80jkjqdAohGhhAbtdfrlnIzrR76hROuXe+vxs1dn++FwV/67c1hQuxCERD3JyqEfnw==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/nm1": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@gi-types/nm1/-/nm1-1.38.1.tgz", + "integrity": "sha512-TZNDTSxPRFjv+kjI/RyqLBO9Rb1FVDuUTfUyB9znGuj4NnPvQ5lzLRlvSG5SIyRYbeu1hik7lGteK+mM/NslVQ==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/notify0": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@gi-types/notify0/-/notify0-0.7.2.tgz", + "integrity": "sha512-cEhvHAffDmZS/Jy5mB+ZrSRMr+2DOt0exfmsHFodbG1nS44kpMaTtpKMHf1uwLuoUTjzdUx/XoTSHuHZsYQ+sw==", + "dev": true, + "dependencies": { + "@gi-types/gdkpixbuf2": "^2.0.2", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/pango": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@gi-types/pango/-/pango-1.0.5.tgz", + "integrity": "sha512-FcDRuMaiYwczKcxE11ZErlOCcbBQX69d/u2hKFkhpZxQrZk0fBNTGLbwJ/7QwAoQw4RsrCzYSOZ4TX0phLPQQw==", + "dev": true, + "dependencies": { + "@gi-types/cairo": "^1.0.3", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/harfbuzz": "^0.0.0" + } + }, + "node_modules/@gi-types/pango1": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@gi-types/pango1/-/pango1-1.50.1.tgz", + "integrity": "sha512-SdpoZ5Fcz7Zo8Tal5Ap4Z/4SMqpo/PspORIadxmNZ3kafFOwK7TPvdWiUIBgua+i3uWiLnreKCGD/6rLJLbw3g==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1", + "@gi-types/harfbuzz2": "^4.4.2" + } + }, + "node_modules/@gi-types/polkit": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/polkit/-/polkit-1.0.6.tgz", + "integrity": "sha512-l7FLcXXzN0ttWjRWIblHHhwZje+jzPdLbXKPNJtRmllBe2ISxVNU31Y+yxxdyXTO1JHr2R543isWEGu2yEmmAA==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "node_modules/@gi-types/polkit1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/polkit1/-/polkit1-1.0.1.tgz", + "integrity": "sha512-M+9OPCx0jeOGe28uN6fegTsYOMuNnjZbgw1sJp6wXRRswihaceqHvuKSePo7dhyC+rx8knS04Cde1rYFQlZiBg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/polkitagent": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/polkitagent/-/polkitagent-1.0.3.tgz", + "integrity": "sha512-x/u0LmV6EC8TACtxlKMIqyAYEur0FRF5P//1wMrBjWdbD0pfBm+KxMvXNCIjs3pHJ1lAZRc9t7Lo4HE03ICPew==", + "dev": true, + "dependencies": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/polkit": "^1.0.6" + } + }, + "node_modules/@gi-types/polkitagent1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/polkitagent1/-/polkitagent1-1.0.1.tgz", + "integrity": "sha512-klFuDVAoFomKpQBx6JtigNhAiuMnkeKEUj8pewR+n3wraVFJ1K/pyT/O14UYR6Qgl8smOBFybyQv15tgc35HnA==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1", + "@gi-types/polkit1": "^1.0.1" + } + }, + "node_modules/@gi-types/rsvg2": { + "version": "2.54.2", + "resolved": "https://registry.npmjs.org/@gi-types/rsvg2/-/rsvg2-2.54.2.tgz", + "integrity": "sha512-FVrNq2JdQZLaUmJeeIGfF79EHzbcd8TqUlaAH8y8f2Yy7Gc4FyBqC+BtnSJ2bN65tus2khPN3YYqVc+YDDIZLw==", + "dev": true, + "dependencies": { + "@gi-types/cairo1": "^1.0.1", + "@gi-types/gdkpixbuf2": "^2.0.2", + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/shell": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@gi-types/shell/-/shell-0.1.6.tgz", + "integrity": "sha512-CFM3e4D02ZqVtOwE9PtPgp+qrxN25vNYXUKFrWj5BZtlrOnie1uC03+riu7f4zSpLfOW8BWX4KpZxycoH9AUbw==", + "dev": true, + "dependencies": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/clutter": "^7.0.6", + "@gi-types/clutterx11": "^7.0.2", + "@gi-types/gcr": "^3.38.7", + "@gi-types/gdkpixbuf": "^2.0.5", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/gtk": "^3.24.8", + "@gi-types/gvc": "^1.0.2", + "@gi-types/json": "^1.6.5", + "@gi-types/meta": "^3.38.5", + "@gi-types/nm": "^1.28.9", + "@gi-types/polkitagent": "^1.0.3", + "@gi-types/st": "^1.0.6" + } + }, + "node_modules/@gi-types/soup2": { + "version": "2.74.1", + "resolved": "https://registry.npmjs.org/@gi-types/soup2/-/soup2-2.74.1.tgz", + "integrity": "sha512-K11KSN032DFAc3RTTBtPiZ9utWqw4CJ7wUAG1cMRdQtRBD44Xi2o2r9RuZkqUhT12V0vU7ZafF8Apb6PM6AOpg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/st": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/st/-/st-1.0.6.tgz", + "integrity": "sha512-xXFs7t19Jmy/GVC9CJFk8oByI546BZdGG/JXx+wN54YG8d+1nX3RiZ2nVNgp2y1gYhJDTcHBRZFnPs+7QUduoQ==", + "dev": true, + "dependencies": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/cally": "^7.0.3", + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/gtk": "^3.24.8", + "@gi-types/json": "^1.6.5", + "@gi-types/meta": "^3.38.5", + "@gi-types/pango": "^1.0.5" + } + }, + "node_modules/@gi-types/telepathyglib0": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@gi-types/telepathyglib0/-/telepathyglib0-0.12.1.tgz", + "integrity": "sha512-c/etfKk0Fdts4GCu0M9cVn13Xev5jqgfwWiFiwi61Y9wUNFw9AlXEhth2FlcXhPuhI58EvhWgvuiizooAVnT0w==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/telepathylogger0": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@gi-types/telepathylogger0/-/telepathylogger0-0.2.1.tgz", + "integrity": "sha512-FSgUMaXT4W9AL8qrmiHxGw6/D/78yURmKyPhIQP8abn64aiTizktDrViMO/8xaV86NMvM31dotnOU0gWx2IgUg==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1", + "@gi-types/telepathyglib0": "^0.12.1" + } + }, + "node_modules/@gi-types/upowerglib1": { + "version": "0.99.1", + "resolved": "https://registry.npmjs.org/@gi-types/upowerglib1/-/upowerglib1-0.99.1.tgz", + "integrity": "sha512-51DF1nxsMbr3n1VAidfuDuyc7CrotflWfXfTj100+CooOvYsLcH6FbjkbkLGTP9yFGPQO3ahbsr12AeYaJkEQA==", + "dev": true, + "dependencies": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@gi-types/xlib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/xlib/-/xlib-2.0.0.tgz", + "integrity": "sha512-YHNrcCw/i8QX3sh5bZ+SN7xEDrmhKIqAoUh/JgiDyPqqH2du+BpQUZy/e0oM0AIZWPNFKgD5M5Bq0+1MvVS3IA==", + "dev": true, + "dependencies": { + "@gi-types/gobject": "^2.66.9" + } + }, + "node_modules/@gi-types/xlib2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/xlib2/-/xlib2-2.0.1.tgz", + "integrity": "sha512-AouZupNa8roCOwKbyGuVuOmt7n8YYBprH60q5Ggu9vGVY+AhpTjEbeJBiLkHW74T8587Z3AilRo7cBWnXPM56A==", + "dev": true, + "dependencies": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "dependencies": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "node_modules/@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.3.0.tgz", + "integrity": "sha512-IZYjYZ0ifGSLZbwMqIip/nOamFiWJ9AH+T/GYNZBWkVcyNQOFGtSMoWV7RvY4poYCMZ/4lHzNl796WOSNxmk8A==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.3.0", + "@typescript-eslint/type-utils": "6.3.0", + "@typescript-eslint/utils": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.3.0.tgz", + "integrity": "sha512-ibP+y2Gr6p0qsUkhs7InMdXrwldjxZw66wpcQq9/PzAroM45wdwyu81T+7RibNCh8oc0AgrsyCwJByncY0Ongg==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "6.3.0", + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/typescript-estree": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.3.0.tgz", + "integrity": "sha512-WlNFgBEuGu74ahrXzgefiz/QlVb+qg8KDTpknKwR7hMH+lQygWyx0CQFoUmMn1zDkQjTBBIn75IxtWss77iBIQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.3.0.tgz", + "integrity": "sha512-7Oj+1ox1T2Yc8PKpBvOKWhoI/4rWFd1j7FA/rPE0lbBPXTKjdbtC+7Ev0SeBjEKkIhKWVeZSP+mR7y1Db1CdfQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "6.3.0", + "@typescript-eslint/utils": "6.3.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/types": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.3.0.tgz", + "integrity": "sha512-K6TZOvfVyc7MO9j60MkRNWyFSf86IbOatTKGrpTQnzarDZPYPVy0oe3myTMq7VjhfsUAbNUW8I5s+2lZvtx1gg==", + "dev": true, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.3.0.tgz", + "integrity": "sha512-Xh4NVDaC4eYKY4O3QGPuQNp5NxBAlEvNQYOqJquR2MePNxO11E5K3t5x4M4Mx53IZvtpW+mBxIT0s274fLUocg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.3.0.tgz", + "integrity": "sha512-hLLg3BZE07XHnpzglNBG8P/IXq/ZVXraEbgY7FM0Cnc1ehM8RMdn9mat3LubJ3KBeYXXPxV1nugWbQPjGeJk6Q==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.3.0", + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/typescript-estree": "6.3.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.3.0.tgz", + "integrity": "sha512-kEhRRj7HnvaSjux1J9+7dBen15CdWmDnwrpyiHsFX6Qx2iW5LOBUgNefOFeh2PjWPlNwN8TOn6+4eBU3J/gupw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "6.3.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/comment-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", + "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "dev": true, + "engines": { + "node": ">= 12.0.0" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", + "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "^8.47.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "46.4.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.4.6.tgz", + "integrity": "sha512-z4SWYnJfOqftZI+b3RM9AtWL1vF/sLWE/LlO9yOKDof9yN2+n3zOdOJTGX/pRE/xnPsooOLG2Rq6e4d+XW3lNw==", + "dev": true, + "dependencies": { + "@es-joy/jsdoccomment": "~0.40.1", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.0", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^3.0.1" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "dependencies": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "dependencies": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "dev": true, + "engines": { + "node": ">=16.13.0" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + }, + "dependencies": { + "@aashutoshrathi/word-wrap": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", + "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==", + "dev": true + }, + "@es-joy/jsdoccomment": { + "version": "0.40.1", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.40.1.tgz", + "integrity": "sha512-YORCdZSusAlBrFpZ77pJjc5r1bQs5caPWtAu+WWmiSo+8XaUzseapVrfAtiRFbQWnrBxxLLEwF6f6ZG/UgCQCg==", + "dev": true, + "requires": { + "comment-parser": "1.4.0", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + } + }, + "@eslint-community/eslint-utils": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", + "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==", + "dev": true, + "requires": { + "eslint-visitor-keys": "^3.3.0" + } + }, + "@eslint-community/regexpp": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.6.2.tgz", + "integrity": "sha512-pPTNuaAG3QMH+buKyBIGJs3g/S5y0caxw0ygM3YyE6yJFySwiGGSzA+mM3KJ8QQvzeLh3blwgSonkFjgQdxzMw==", + "dev": true + }, + "@eslint/eslintrc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.2.tgz", + "integrity": "sha512-+wvgpDsrB1YqAMdEUCcnTlpfVBH7Vqn6A/NT3D8WVXFIaKMlErPIZT3oCIAVCOtarRpMtelZLqJeU3t7WY6X6g==", + "dev": true, + "requires": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + } + }, + "@eslint/js": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.47.0.tgz", + "integrity": "sha512-P6omY1zv5MItm93kLM8s2vr1HICJH8v0dvddDhysbIuZ+vcjOHg5Zbkf1mTkcmi2JA9oBG2anOkRnW8WJTS8Og==", + "dev": true + }, + "@gi-types/accountsservice1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/accountsservice1/-/accountsservice1-1.0.1.tgz", + "integrity": "sha512-u2X3p54sl3U5Vptx/DmGVjwf3YdkjqcTaiwPIGBmdhTOcdVBLXbIrVtH50dVkwt6nURASnknOFzVuPBg+jEPCg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/adw1": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@gi-types/adw1/-/adw1-1.1.1.tgz", + "integrity": "sha512-w+NxplCUf2AeVdSwkicbVrIOB5QJd9BQo1BI3Xq+2IZkgMRePIUG9/eGsrvmzxKU00EiH211KEwqGe/2UOmomQ==", + "dev": true, + "requires": { + "@gi-types/gdk4": "^4.0.1", + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/gsk4": "^4.0.1", + "@gi-types/gtk4": "^4.6.1" + } + }, + "@gi-types/atk": { + "version": "2.36.7", + "resolved": "https://registry.npmjs.org/@gi-types/atk/-/atk-2.36.7.tgz", + "integrity": "sha512-PWR/EqgBgqehMfDr48fqvy77yGEKLwtqN0qKnKOtWqDm65LbImoAFydlMx5CUeyE/ZNcGDhQGLimQ0Rk6kVivw==", + "dev": true, + "requires": { + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/atk1": { + "version": "2.36.0", + "resolved": "https://registry.npmjs.org/@gi-types/atk1/-/atk1-2.36.0.tgz", + "integrity": "sha512-lCuoaJwqFDV+V+sf4mOcciLCJitiUsGNg/W2Q5Y+21FycPZqlsjgXb0pimo7gLdq11d1lEfHffh3AkY80+7SHw==", + "dev": true, + "requires": { + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0" + } + }, + "@gi-types/atspi2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/atspi2/-/atspi2-2.0.1.tgz", + "integrity": "sha512-REenxB9zE4J++j92cgqTVKBPS/Pdmlj1JJ38c8t174OObS6nIUyi9B563VyehkfbZN75BZ7sGNihnnsLVsbIrQ==", + "dev": true, + "requires": { + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/base-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/base-types/-/base-types-1.0.0.tgz", + "integrity": "sha512-A+TMfI4f+RgBx5uM5la4tqRrraP8N9B3SDFFPvYyH3h9VMcy03/Fp4X0LVtHuYdAUHmdRR8Z4Got9lwVd975+g==", + "dev": true, + "requires": { + "@gi-types/accountsservice1": "^1.0.0", + "@gi-types/atk1": "*", + "@gi-types/atspi2": "*", + "@gi-types/cairo1": "*", + "@gi-types/dbus1": "*", + "@gi-types/gck1": "*", + "@gi-types/gcr3": "*", + "@gi-types/gdkpixbuf2": "*", + "@gi-types/gdm1": "*", + "@gi-types/geoclue2": "*", + "@gi-types/gio2": "*", + "@gi-types/girepository2": "^1.68.0", + "@gi-types/glib2": "*", + "@gi-types/gmodule2": "*", + "@gi-types/gobject2": "*", + "@gi-types/harfbuzz2": "*", + "@gi-types/ibus1": "*", + "@gi-types/json1": "*", + "@gi-types/malcontent0": "*", + "@gi-types/modemmanager1": "*", + "@gi-types/nm1": "*", + "@gi-types/notify0": "*", + "@gi-types/pango1": "*", + "@gi-types/polkit1": "*", + "@gi-types/polkitagent1": "*", + "@gi-types/rsvg2": "*", + "@gi-types/soup2": "*", + "@gi-types/telepathyglib0": "*", + "@gi-types/telepathylogger0": "*", + "@gi-types/upowerglib1": "*", + "@gi-types/xlib2": "*" + } + }, + "@gi-types/cairo": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/cairo/-/cairo-1.0.3.tgz", + "integrity": "sha512-IT4JbSYgMj2aJbx7Yy8nzHgga2vX5i5OHR2+vXKc9vg/wrnExHiSbacWbxO7CZdk6YBqpH/7cjOa4bkTmBN8tg==", + "dev": true, + "requires": { + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/cairo1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/cairo1/-/cairo1-1.0.1.tgz", + "integrity": "sha512-Q92XKfTWVmtF3LBX9Kx7TkdRjeaZeb8llC01p9WgbkVfFX1d2VO4LrqYgeNkThwmJpscHzDzGF0uv5njsfd4xg==", + "dev": true, + "requires": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/cally": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/cally/-/cally-7.0.3.tgz", + "integrity": "sha512-mezisdoGvnewxncjpejrAa2XO0WIYsv+jD+olQu49cSU3BIBI9vmVMd1cmkngTGqombkd/2C9KwFMq1ZvEOhiQ==", + "dev": true, + "requires": { + "@gi-types/atk": "^2.36.7", + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/clutter": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/clutter/-/clutter-7.0.6.tgz", + "integrity": "sha512-hLQ5K+wnq40B1VWFVJPtENw82at2SHHE1CN+BCagjO6L9PpAvO9sCV9uUSbl9K5zrzcfgxYdwFrXW5NP6wSKTg==", + "dev": true, + "requires": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/graphene": "^1.0.1", + "@gi-types/json": "^1.6.5", + "@gi-types/pango": "^1.0.5" + } + }, + "@gi-types/clutterx11": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/clutterx11/-/clutterx11-7.0.2.tgz", + "integrity": "sha512-xYJPu1tszpuEkfc6L28DBm2gsuC3pemfi8ixm41I/RI2iIHuhxGEV/PtyOIy2pSM1ZmdItbjRQWq7k0Jz5oggQ==", + "dev": true, + "requires": { + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/cogl": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/cogl/-/cogl-7.0.3.tgz", + "integrity": "sha512-yub8mzFnUvZfcBGCxeTS/mShT9pOWcx/FaVmBU+NCEdUlv6LwA+kaVeDJx9SjVFOIrybsJ06q7f/QedkQOfUQQ==", + "dev": true, + "requires": { + "@gi-types/cairo": "^1.0.3", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/graphene": "^1.0.1" + } + }, + "@gi-types/coglpango": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/coglpango/-/coglpango-7.0.2.tgz", + "integrity": "sha512-YH6lmMZeo5HU9mhekIKAtj1OCvK2D+l8hzr1NDz5UIxR1K3oAYqu3sMX1EI0dB24pxcn+mqq/J4xpMO50sCnJA==", + "dev": true, + "requires": { + "@gi-types/cogl": "^7.0.3", + "@gi-types/gobject": "^2.66.8", + "@gi-types/pango": "^1.0.5" + } + }, + "@gi-types/dbus1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/dbus1/-/dbus1-1.0.1.tgz", + "integrity": "sha512-gFM9UrQCQXxpFB5hg2EW8CRCgY5OTmfidBEh085rux/dqFyGR8lSfDIyN0/RZXrD8rggoQ6JteNhbmNxtQkWmw==", + "dev": true, + "requires": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/freetype22": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/freetype22/-/freetype22-2.0.1.tgz", + "integrity": "sha512-3p4XvTSn6o1UCGpla/1KhvMIwZkgIyBtGErbnXuAvGzULtlseQjX2IljY4fi2FKtFd9ngRtIIF71inRPUIj+1w==", + "dev": true, + "requires": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gck": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/gck/-/gck-1.0.6.tgz", + "integrity": "sha512-yjbVHprwZ6KE3B/JqMFgHJvAgULuSs0obEWMJFeSpXHgke5Vab8a2Gi0pXdwU5AHVD1rVxhNy/8dkRdiIucwoA==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/gck1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gck1/-/gck1-1.0.1.tgz", + "integrity": "sha512-EkLhQvsb/qHBffTJ5j9aK/KX2nE8LTHDbPSMrXtaTWz3yQYuRDqTyXJWVVUxfcXwKliE9GYyqEOAiXlXCamFHQ==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gcr": { + "version": "3.38.8", + "resolved": "https://registry.npmjs.org/@gi-types/gcr/-/gcr-3.38.8.tgz", + "integrity": "sha512-UuIStqiXrcXA9RmQZJ/T0K5WpFt5GtwVgLooSOVMflnn28ltx7gA0OVD/keUOjGqVPtubr102Jvzu+KNSGbDxQ==", + "dev": true, + "requires": { + "@gi-types/gck": "^1.0.6", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.9" + } + }, + "@gi-types/gcr3": { + "version": "3.41.1", + "resolved": "https://registry.npmjs.org/@gi-types/gcr3/-/gcr3-3.41.1.tgz", + "integrity": "sha512-sZWGbOQlbtwNoggDDBqFxD1lAyGpIEg7TR5RVVrcQfaMwo7yIkRahvXCy7XCoYnWsEmAw46fEdUI5408Ykn8/g==", + "dev": true, + "requires": { + "@gi-types/gck1": "^1.0.1", + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gdesktopenums": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/gdesktopenums/-/gdesktopenums-3.0.3.tgz", + "integrity": "sha512-2Ng2uCCZcgASARPcIxapkTFrkM5std3ANUeW1pc4CJ4ZXLYgwSad8IQxJ4wB7hXkhZR9CYXmoA4KBmScjaHfyA==", + "dev": true, + "requires": { + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/gdk": { + "version": "3.24.7", + "resolved": "https://registry.npmjs.org/@gi-types/gdk/-/gdk-3.24.7.tgz", + "integrity": "sha512-t50PlPeiqeNNQvXcKuso7/aiy1zm2zNtDgXZgcmtCfjk2u9m4m5gC4/kePzfYtSBqo15FZdv747ESRLC0zNk6Q==", + "dev": true, + "requires": { + "@gi-types/cairo": "^1.0.3", + "@gi-types/gdkpixbuf": "^2.0.5", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/pango": "^1.0.5" + } + }, + "@gi-types/gdk4": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gdk4/-/gdk4-4.0.1.tgz", + "integrity": "sha512-Msf4bWdw8A0qorTvbvWgCwMno8nUCGLiDcEhNI6iqh6o8IJKchID9a51j6KeqpHkwSPZAcaR2TKM+CVafa4pOw==", + "dev": true, + "requires": { + "@gi-types/cairo1": "^1.0.0", + "@gi-types/gdkpixbuf2": "^2.0.0", + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/pango1": "^1.0.0" + } + }, + "@gi-types/gdkpixbuf": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@gi-types/gdkpixbuf/-/gdkpixbuf-2.0.5.tgz", + "integrity": "sha512-bLw6CFCbM0SNi30vkbupoVxE7T5hz+V6DQHLa1Sh0yYwm8OTQ8jYofZNTabfjBiTR3Z4dxtN65DkIpJoD+9fuQ==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/gdkpixbuf2": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/gdkpixbuf2/-/gdkpixbuf2-2.0.2.tgz", + "integrity": "sha512-vwopihyKOjJszDPvMj+T2XO6hMWYRmhW/uSrfCeCudMhzmJBrpIzmRZTYfeTgMMDU8cuWydCjacbiD7hSio0tw==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gmodule2": "^2.0.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gdm1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gdm1/-/gdm1-1.0.1.tgz", + "integrity": "sha512-XfrIzC4fI8SxnjjJ16d3PA5FVMOnkyZrEdg4fYtLK4m/GdytrivAgxsousXlnlIEXuDUA4Nss7ycja6TIa70NA==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/geoclue2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/geoclue2/-/geoclue2-2.0.1.tgz", + "integrity": "sha512-7X7URSNs3fdrER2uUdWmU4FaQry7aBaCyCGTNitmAwKigijv1OihI6oDyIx9Y24rmwrz6Im2+tKrg0bugnxJjQ==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gio": { + "version": "2.66.9", + "resolved": "https://registry.npmjs.org/@gi-types/gio/-/gio-2.66.9.tgz", + "integrity": "sha512-hRqJqQSQ4jLjUSiS7XZ7pXvIdu/+ZuFvIGPqrLFXyYa+aTenHiYg/EaLR3iPbAzkvpyBLRZk7Pmgs0quoK+Vtg==", + "dev": true, + "requires": { + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/gio2": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/@gi-types/gio2/-/gio2-2.72.1.tgz", + "integrity": "sha512-V3ASxEbq4xBdw/rcROEbNrTqtFtWfwFOcOsXO+48JPIEqZRTJZcf9LX/ATxi9hxaZXogpmOBMsxuyNsd5ZQXyA==", + "dev": true, + "requires": { + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/girepository2": { + "version": "1.68.0", + "resolved": "https://registry.npmjs.org/@gi-types/girepository2/-/girepository2-1.68.0.tgz", + "integrity": "sha512-OYTsgMdQ1UWua6Bvx/rSL29qn9CCZ1rMpoBsfvDs5ol+9nNYsxUIl7AvRIojEk/IbWmYh/K9Kv3Mqi3ntB3aTA==", + "dev": true, + "requires": { + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0" + } + }, + "@gi-types/gjs-environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@gi-types/gjs-environment/-/gjs-environment-1.1.0.tgz", + "integrity": "sha512-WXQ1xwuLou5qobQ76LyYxU5bgZYNbkypm3ZLphNWZLuj8C9MbWmb9BJihdq2IAAyH6+HIrrN6ss1syu7IJZUaA==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0" + } + }, + "@gi-types/glib": { + "version": "2.66.10", + "resolved": "https://registry.npmjs.org/@gi-types/glib/-/glib-2.66.10.tgz", + "integrity": "sha512-x34Y/xngzSS/BjA38/XpZjAzQqdWdUFtEVKsAGIMi4g+2ZES5QvBLkg/vNHwzSeoY/vUf+E/aZ3+6xQZFUzRjg==", + "dev": true, + "requires": { + "@gi-types/gobject": "^2.66.9" + } + }, + "@gi-types/glib2": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/@gi-types/glib2/-/glib2-2.72.1.tgz", + "integrity": "sha512-Mfhs1UGV1a0GOLolqA1xC2oVV5cILbJjGh/M9Db3irqWNIWGM0LUFkoG2nRMBpJ0enhIYbqY9CCpGlMpAnyxOw==", + "dev": true, + "requires": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gmodule2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gmodule2/-/gmodule2-2.0.1.tgz", + "integrity": "sha512-thIYqYL3ALKBLR/zpjrlXro1m9UTv6eAYyuIdpPtmQvlaggBLoUakYl6mzNmqcb22OIlq/8jJrVx+cjFxdPd/A==", + "dev": true, + "requires": { + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/gobject": { + "version": "2.66.9", + "resolved": "https://registry.npmjs.org/@gi-types/gobject/-/gobject-2.66.9.tgz", + "integrity": "sha512-fr6a0W0UfLQiYYuQg4ZY+u4oFgC0QacjffbCBupiVHyC4NVDH2FbWiEVQEb0V52QmSEZTAfRyc/ZgmDzwpUXuw==", + "dev": true, + "requires": { + "@gi-types/glib": "^2.66.9" + } + }, + "@gi-types/gobject2": { + "version": "2.72.1", + "resolved": "https://registry.npmjs.org/@gi-types/gobject2/-/gobject2-2.72.1.tgz", + "integrity": "sha512-1UUD2zzwRo7vJLu0J62L1cgB39wjYNs1+6qS0UBhdLhzkOWK+CnVII9CNh21Z4YBs0Oa2d5REeGUiZO4d7a6Zw==", + "dev": true, + "requires": { + "@gi-types/glib2": "^2.72.1" + } + }, + "@gi-types/graphene": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/graphene/-/graphene-1.0.1.tgz", + "integrity": "sha512-XWzEdAWdvsJH2ieraRhbXqyoFXXG2HP2P8amSpryBsXuWaiessnhsNKgB+pu93cgrW029nnYJMb6AUJodASMrw==", + "dev": true, + "requires": { + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/graphene1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/graphene1/-/graphene1-1.0.1.tgz", + "integrity": "sha512-O5cJG/eIi912eF1zcaXrSur25LgMJDU+VhFDnYuO4Latj32eypFJf5Sw3KOZvN2EoctIBxjutbXLOxYYKMBhHA==", + "dev": true, + "requires": { + "@gi-types/gobject2": "^2.68.0" + } + }, + "@gi-types/gsk4": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/gsk4/-/gsk4-4.0.1.tgz", + "integrity": "sha512-/OtGBQwwtyQw7vjkfC4/P2pQf9hrtB89sXhDyKQG5B2UJ/LTAQQJNK7vu4u2W0Y922WK/zqrzimBy38JUoPO5Q==", + "dev": true, + "requires": { + "@gi-types/cairo1": "^1.0.0", + "@gi-types/gdk4": "^4.0.1", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/graphene1": "^1.0.1", + "@gi-types/pango1": "^1.0.0" + } + }, + "@gi-types/gtk": { + "version": "3.24.9", + "resolved": "https://registry.npmjs.org/@gi-types/gtk/-/gtk-3.24.9.tgz", + "integrity": "sha512-U2LH7hKlIvPAvjVe0eQX8s6MDjYaFKCRpAGmwR3lIoBPIdvYOyy76odGY1suFKrepBa97iW6JMbEcv/0f2D98w==", + "dev": true, + "requires": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/gdk": "^3.24.7", + "@gi-types/gdkpixbuf": "^2.0.5", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.9", + "@gi-types/pango": "^1.0.5", + "@gi-types/xlib": "^2.0.0" + } + }, + "@gi-types/gtk4": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/@gi-types/gtk4/-/gtk4-4.6.1.tgz", + "integrity": "sha512-WtdHV2S/xParqwJX9BJ/OsGTzoEorEhVAP0W2eJpUk5rqfSlzIXHnNvFvs9T5sT43fRZmefVd3Id1EbxsHKeLg==", + "dev": true, + "requires": { + "@gi-types/cairo1": "^1.0.0", + "@gi-types/gdk4": "^4.0.1", + "@gi-types/gdkpixbuf2": "^2.0.0", + "@gi-types/gio2": "^2.68.0", + "@gi-types/glib2": "^2.68.0", + "@gi-types/gobject2": "^2.68.0", + "@gi-types/graphene1": "^1.0.1", + "@gi-types/gsk4": "^4.0.1", + "@gi-types/pango1": "^1.50.1" + } + }, + "@gi-types/gtk4-types": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/gtk4-types/-/gtk4-types-1.0.0.tgz", + "integrity": "sha512-Y7SNMhAX9bUBK/HfJbidMePIjsa/7B6WroLR2nZF3afIzqjYgEugmbYWC85516z0+c1/3Z4eXC8V3j1CleXmGQ==", + "dev": true, + "requires": { + "@gi-types/gdk4": "*", + "@gi-types/graphene1": "*", + "@gi-types/gsk4": "*", + "@gi-types/gtk4": "*" + } + }, + "@gi-types/gvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@gi-types/gvc/-/gvc-1.0.2.tgz", + "integrity": "sha512-u5QMX4nI6FJSvnc/Se0JObycRlMznTz6BlZ2CM84i9ePhL0K6vplM0UIeTfWDxJiwS+M9mofQnA10uWt8CZRhA==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/harfbuzz": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/harfbuzz/-/harfbuzz-0.0.0.tgz", + "integrity": "sha512-pqhpLBN5aYl2P6yOPo0HCZTLmcTDiynGkEgfEq1yPwY0qhJpuEJYmaVUzwb7RrQrt7absVZP7ClhhO8TYiporw==", + "dev": true, + "requires": { + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/harfbuzz2": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@gi-types/harfbuzz2/-/harfbuzz2-4.4.2.tgz", + "integrity": "sha512-jsntc6SthoOdw4VSetTOKFI0BM1ccDISP1JQhnL5NETrVuYGnNAwRZ2oDIujuG0M70bHDv/MWKiZnmJxXNPrKw==", + "dev": true, + "requires": { + "@gi-types/freetype22": "^2.0.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/ibus1": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@gi-types/ibus1/-/ibus1-1.5.1.tgz", + "integrity": "sha512-pwPk1H0zlf3u5DQ0g3ENfN4PY9U1mu5N6dRHYocwXsmENs0EEZvYrHa+SfUC2DEpsIvvMoAfm2lwKAVQ7KqNRg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/json": { + "version": "1.6.6", + "resolved": "https://registry.npmjs.org/@gi-types/json/-/json-1.6.6.tgz", + "integrity": "sha512-YtnjubEVEmnGirOlcwNkwD9hftaOKuJv2t0HnNFXcHuBCZpD72Jt5iuUtyFFvF7cduVR/jAXh71fw5U3kMYB8g==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.9" + } + }, + "@gi-types/json1": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/@gi-types/json1/-/json1-1.6.1.tgz", + "integrity": "sha512-6DpNf3PTYYKbL96U76R/mWZuAXv32cI0ZDalyKAciKeh9pczHjlnNdjq8h6lDKJc5vKxviHW9TsOdtB3NaXuqQ==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/malcontent0": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/malcontent0/-/malcontent0-0.0.1.tgz", + "integrity": "sha512-zB/M/ew8P1ZEqeXChK9b5PAoqq5TrmkTZ3NkZ0R/DPX/xt7aZRNPwdxrFt9enzrlKImjthcctp/tpk48MgGoKg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/meta": { + "version": "3.38.5", + "resolved": "https://registry.npmjs.org/@gi-types/meta/-/meta-3.38.5.tgz", + "integrity": "sha512-EzYGvZu58j/hDZv/zQX4ooL0Pq4TEP2Okhn0JlVmGpFIB65ZWOkCHQqj3LODdpp327+cWMNYkG5WHZXJAmtuZQ==", + "dev": true, + "requires": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/coglpango": "^7.0.2", + "@gi-types/gdesktopenums": "^3.0.3", + "@gi-types/gdk": "^3.24.7", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/gtk": "^3.24.8", + "@gi-types/json": "^1.6.5", + "@gi-types/pango": "^1.0.5" + } + }, + "@gi-types/modemmanager1": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@gi-types/modemmanager1/-/modemmanager1-1.18.1.tgz", + "integrity": "sha512-eQgs3B0S61qtrIIoa9APcmszDGsVEbxZJwlAOak/4ifF4HYrF42fsTDQslzGoAe0aDe9B/FJe2ycR8LgMgquhg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/nm": { + "version": "1.28.9", + "resolved": "https://registry.npmjs.org/@gi-types/nm/-/nm-1.28.9.tgz", + "integrity": "sha512-sZd0HWWJ4aeXTrbnmTlN80jkjqdAohGhhAbtdfrlnIzrR76hROuXe+vxs1dn++FwV/67c1hQuxCERD3JyqEfnw==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/nm1": { + "version": "1.38.1", + "resolved": "https://registry.npmjs.org/@gi-types/nm1/-/nm1-1.38.1.tgz", + "integrity": "sha512-TZNDTSxPRFjv+kjI/RyqLBO9Rb1FVDuUTfUyB9znGuj4NnPvQ5lzLRlvSG5SIyRYbeu1hik7lGteK+mM/NslVQ==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/notify0": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@gi-types/notify0/-/notify0-0.7.2.tgz", + "integrity": "sha512-cEhvHAffDmZS/Jy5mB+ZrSRMr+2DOt0exfmsHFodbG1nS44kpMaTtpKMHf1uwLuoUTjzdUx/XoTSHuHZsYQ+sw==", + "dev": true, + "requires": { + "@gi-types/gdkpixbuf2": "^2.0.2", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/pango": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@gi-types/pango/-/pango-1.0.5.tgz", + "integrity": "sha512-FcDRuMaiYwczKcxE11ZErlOCcbBQX69d/u2hKFkhpZxQrZk0fBNTGLbwJ/7QwAoQw4RsrCzYSOZ4TX0phLPQQw==", + "dev": true, + "requires": { + "@gi-types/cairo": "^1.0.3", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/harfbuzz": "^0.0.0" + } + }, + "@gi-types/pango1": { + "version": "1.50.1", + "resolved": "https://registry.npmjs.org/@gi-types/pango1/-/pango1-1.50.1.tgz", + "integrity": "sha512-SdpoZ5Fcz7Zo8Tal5Ap4Z/4SMqpo/PspORIadxmNZ3kafFOwK7TPvdWiUIBgua+i3uWiLnreKCGD/6rLJLbw3g==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1", + "@gi-types/harfbuzz2": "^4.4.2" + } + }, + "@gi-types/polkit": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/polkit/-/polkit-1.0.6.tgz", + "integrity": "sha512-l7FLcXXzN0ttWjRWIblHHhwZje+jzPdLbXKPNJtRmllBe2ISxVNU31Y+yxxdyXTO1JHr2R543isWEGu2yEmmAA==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8" + } + }, + "@gi-types/polkit1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/polkit1/-/polkit1-1.0.1.tgz", + "integrity": "sha512-M+9OPCx0jeOGe28uN6fegTsYOMuNnjZbgw1sJp6wXRRswihaceqHvuKSePo7dhyC+rx8knS04Cde1rYFQlZiBg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/polkitagent": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@gi-types/polkitagent/-/polkitagent-1.0.3.tgz", + "integrity": "sha512-x/u0LmV6EC8TACtxlKMIqyAYEur0FRF5P//1wMrBjWdbD0pfBm+KxMvXNCIjs3pHJ1lAZRc9t7Lo4HE03ICPew==", + "dev": true, + "requires": { + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/polkit": "^1.0.6" + } + }, + "@gi-types/polkitagent1": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/polkitagent1/-/polkitagent1-1.0.1.tgz", + "integrity": "sha512-klFuDVAoFomKpQBx6JtigNhAiuMnkeKEUj8pewR+n3wraVFJ1K/pyT/O14UYR6Qgl8smOBFybyQv15tgc35HnA==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1", + "@gi-types/polkit1": "^1.0.1" + } + }, + "@gi-types/rsvg2": { + "version": "2.54.2", + "resolved": "https://registry.npmjs.org/@gi-types/rsvg2/-/rsvg2-2.54.2.tgz", + "integrity": "sha512-FVrNq2JdQZLaUmJeeIGfF79EHzbcd8TqUlaAH8y8f2Yy7Gc4FyBqC+BtnSJ2bN65tus2khPN3YYqVc+YDDIZLw==", + "dev": true, + "requires": { + "@gi-types/cairo1": "^1.0.1", + "@gi-types/gdkpixbuf2": "^2.0.2", + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/shell": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@gi-types/shell/-/shell-0.1.6.tgz", + "integrity": "sha512-CFM3e4D02ZqVtOwE9PtPgp+qrxN25vNYXUKFrWj5BZtlrOnie1uC03+riu7f4zSpLfOW8BWX4KpZxycoH9AUbw==", + "dev": true, + "requires": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/clutter": "^7.0.6", + "@gi-types/clutterx11": "^7.0.2", + "@gi-types/gcr": "^3.38.7", + "@gi-types/gdkpixbuf": "^2.0.5", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/gtk": "^3.24.8", + "@gi-types/gvc": "^1.0.2", + "@gi-types/json": "^1.6.5", + "@gi-types/meta": "^3.38.5", + "@gi-types/nm": "^1.28.9", + "@gi-types/polkitagent": "^1.0.3", + "@gi-types/st": "^1.0.6" + } + }, + "@gi-types/soup2": { + "version": "2.74.1", + "resolved": "https://registry.npmjs.org/@gi-types/soup2/-/soup2-2.74.1.tgz", + "integrity": "sha512-K11KSN032DFAc3RTTBtPiZ9utWqw4CJ7wUAG1cMRdQtRBD44Xi2o2r9RuZkqUhT12V0vU7ZafF8Apb6PM6AOpg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/st": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@gi-types/st/-/st-1.0.6.tgz", + "integrity": "sha512-xXFs7t19Jmy/GVC9CJFk8oByI546BZdGG/JXx+wN54YG8d+1nX3RiZ2nVNgp2y1gYhJDTcHBRZFnPs+7QUduoQ==", + "dev": true, + "requires": { + "@gi-types/atk": "^2.36.7", + "@gi-types/cairo": "^1.0.3", + "@gi-types/cally": "^7.0.3", + "@gi-types/clutter": "^7.0.6", + "@gi-types/cogl": "^7.0.3", + "@gi-types/gio": "^2.66.9", + "@gi-types/glib": "^2.66.9", + "@gi-types/gobject": "^2.66.8", + "@gi-types/gtk": "^3.24.8", + "@gi-types/json": "^1.6.5", + "@gi-types/meta": "^3.38.5", + "@gi-types/pango": "^1.0.5" + } + }, + "@gi-types/telepathyglib0": { + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/@gi-types/telepathyglib0/-/telepathyglib0-0.12.1.tgz", + "integrity": "sha512-c/etfKk0Fdts4GCu0M9cVn13Xev5jqgfwWiFiwi61Y9wUNFw9AlXEhth2FlcXhPuhI58EvhWgvuiizooAVnT0w==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/telepathylogger0": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@gi-types/telepathylogger0/-/telepathylogger0-0.2.1.tgz", + "integrity": "sha512-FSgUMaXT4W9AL8qrmiHxGw6/D/78yURmKyPhIQP8abn64aiTizktDrViMO/8xaV86NMvM31dotnOU0gWx2IgUg==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/glib2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1", + "@gi-types/telepathyglib0": "^0.12.1" + } + }, + "@gi-types/upowerglib1": { + "version": "0.99.1", + "resolved": "https://registry.npmjs.org/@gi-types/upowerglib1/-/upowerglib1-0.99.1.tgz", + "integrity": "sha512-51DF1nxsMbr3n1VAidfuDuyc7CrotflWfXfTj100+CooOvYsLcH6FbjkbkLGTP9yFGPQO3ahbsr12AeYaJkEQA==", + "dev": true, + "requires": { + "@gi-types/gio2": "^2.72.1", + "@gi-types/gobject2": "^2.72.1" + } + }, + "@gi-types/xlib": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@gi-types/xlib/-/xlib-2.0.0.tgz", + "integrity": "sha512-YHNrcCw/i8QX3sh5bZ+SN7xEDrmhKIqAoUh/JgiDyPqqH2du+BpQUZy/e0oM0AIZWPNFKgD5M5Bq0+1MvVS3IA==", + "dev": true, + "requires": { + "@gi-types/gobject": "^2.66.9" + } + }, + "@gi-types/xlib2": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@gi-types/xlib2/-/xlib2-2.0.1.tgz", + "integrity": "sha512-AouZupNa8roCOwKbyGuVuOmt7n8YYBprH60q5Ggu9vGVY+AhpTjEbeJBiLkHW74T8587Z3AilRo7cBWnXPM56A==", + "dev": true, + "requires": { + "@gi-types/gobject2": "^2.72.1" + } + }, + "@humanwhocodes/config-array": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.10.tgz", + "integrity": "sha512-KVVjQmNUepDVGXNuoRRdmmEjruj0KfiGSbS8LVc12LMsWDQzRXJ0qdhN8L8uUigKpfEHRhlaQFY0ib1tnUbNeQ==", + "dev": true, + "requires": { + "@humanwhocodes/object-schema": "^1.2.1", + "debug": "^4.1.1", + "minimatch": "^3.0.5" + } + }, + "@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true + }, + "@humanwhocodes/object-schema": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.1.tgz", + "integrity": "sha512-ZnQMnLV4e7hDlUvw8H+U8ASL02SS2Gn6+9Ac3wGGLIe7+je2AeAOxPY+izIPJDfFDb7eDjev0Us8MO1iFRN8hA==", + "dev": true + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@types/json-schema": { + "version": "7.0.12", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.12.tgz", + "integrity": "sha512-Hr5Jfhc9eYOQNPYO5WLDq/n4jqijdHNlDXjuAQkkt+mWdQR+XJToOHrsD4cPaMXpn6KO7y2+wM8AZEs8VpBLVA==", + "dev": true + }, + "@types/semver": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.5.0.tgz", + "integrity": "sha512-G8hZ6XJiHnuhQKR7ZmysCeJWE08o8T0AXtk5darsCaTVsYZhhgUrq53jizaR2FvsoeCwJhlmwTjkXBY5Pn/ZHw==", + "dev": true + }, + "@typescript-eslint/eslint-plugin": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.3.0.tgz", + "integrity": "sha512-IZYjYZ0ifGSLZbwMqIip/nOamFiWJ9AH+T/GYNZBWkVcyNQOFGtSMoWV7RvY4poYCMZ/4lHzNl796WOSNxmk8A==", + "dev": true, + "requires": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.3.0", + "@typescript-eslint/type-utils": "6.3.0", + "@typescript-eslint/utils": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "natural-compare-lite": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/parser": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.3.0.tgz", + "integrity": "sha512-ibP+y2Gr6p0qsUkhs7InMdXrwldjxZw66wpcQq9/PzAroM45wdwyu81T+7RibNCh8oc0AgrsyCwJByncY0Ongg==", + "dev": true, + "requires": { + "@typescript-eslint/scope-manager": "6.3.0", + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/typescript-estree": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0", + "debug": "^4.3.4" + } + }, + "@typescript-eslint/scope-manager": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.3.0.tgz", + "integrity": "sha512-WlNFgBEuGu74ahrXzgefiz/QlVb+qg8KDTpknKwR7hMH+lQygWyx0CQFoUmMn1zDkQjTBBIn75IxtWss77iBIQ==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0" + } + }, + "@typescript-eslint/type-utils": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.3.0.tgz", + "integrity": "sha512-7Oj+1ox1T2Yc8PKpBvOKWhoI/4rWFd1j7FA/rPE0lbBPXTKjdbtC+7Ev0SeBjEKkIhKWVeZSP+mR7y1Db1CdfQ==", + "dev": true, + "requires": { + "@typescript-eslint/typescript-estree": "6.3.0", + "@typescript-eslint/utils": "6.3.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/types": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.3.0.tgz", + "integrity": "sha512-K6TZOvfVyc7MO9j60MkRNWyFSf86IbOatTKGrpTQnzarDZPYPVy0oe3myTMq7VjhfsUAbNUW8I5s+2lZvtx1gg==", + "dev": true + }, + "@typescript-eslint/typescript-estree": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.3.0.tgz", + "integrity": "sha512-Xh4NVDaC4eYKY4O3QGPuQNp5NxBAlEvNQYOqJquR2MePNxO11E5K3t5x4M4Mx53IZvtpW+mBxIT0s274fLUocg==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/visitor-keys": "6.3.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + } + }, + "@typescript-eslint/utils": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.3.0.tgz", + "integrity": "sha512-hLLg3BZE07XHnpzglNBG8P/IXq/ZVXraEbgY7FM0Cnc1ehM8RMdn9mat3LubJ3KBeYXXPxV1nugWbQPjGeJk6Q==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.3.0", + "@typescript-eslint/types": "6.3.0", + "@typescript-eslint/typescript-estree": "6.3.0", + "semver": "^7.5.4" + } + }, + "@typescript-eslint/visitor-keys": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.3.0.tgz", + "integrity": "sha512-kEhRRj7HnvaSjux1J9+7dBen15CdWmDnwrpyiHsFX6Qx2iW5LOBUgNefOFeh2PjWPlNwN8TOn6+4eBU3J/gupw==", + "dev": true, + "requires": { + "@typescript-eslint/types": "6.3.0", + "eslint-visitor-keys": "^3.4.1" + } + }, + "acorn": { + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", + "dev": true + }, + "acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "requires": {} + }, + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "requires": { + "color-convert": "^2.0.1" + } + }, + "are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", + "dev": true + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "requires": { + "fill-range": "^7.0.1" + } + }, + "builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "comment-parser": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.0.tgz", + "integrity": "sha512-QLyTNiZ2KDOibvFPlZ6ZngVsZ/0gYnE6uTXi5aoDg8ed3AkJAz4sEje3Y8a29hQ1s6A99MZXe47fLAXQ1rTqaw==", + "dev": true + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "cross-spawn": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", + "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "requires": { + "ms": "2.1.2" + } + }, + "deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "requires": { + "path-type": "^4.0.0" + } + }, + "doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "requires": { + "esutils": "^2.0.2" + } + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true + }, + "eslint": { + "version": "8.47.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.47.0.tgz", + "integrity": "sha512-spUQWrdPt+pRVP1TTJLmfRNJJHHZryFmptzcafwSvHsceV81djHOdnEeDmkdotZyLNjDhrOasNK8nikkoG1O8Q==", + "dev": true, + "requires": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.2", + "@eslint/js": "^8.47.0", + "@humanwhocodes/config-array": "^0.11.10", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + } + }, + "eslint-plugin-jsdoc": { + "version": "46.4.6", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.4.6.tgz", + "integrity": "sha512-z4SWYnJfOqftZI+b3RM9AtWL1vF/sLWE/LlO9yOKDof9yN2+n3zOdOJTGX/pRE/xnPsooOLG2Rq6e4d+XW3lNw==", + "dev": true, + "requires": { + "@es-joy/jsdoccomment": "~0.40.1", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.0", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^3.0.1" + } + }, + "eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + } + }, + "eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true + }, + "espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dev": true, + "requires": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + } + }, + "esquery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz", + "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==", + "dev": true, + "requires": { + "estraverse": "^5.1.0" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "requires": { + "estraverse": "^5.2.0" + } + }, + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", + "integrity": "sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "dependencies": { + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "requires": { + "is-glob": "^4.0.1" + } + } + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "requires": { + "flat-cache": "^3.0.4" + } + }, + "fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "requires": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + } + }, + "flat-cache": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", + "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "dev": true, + "requires": { + "flatted": "^3.1.0", + "rimraf": "^3.0.2" + } + }, + "flatted": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.7.tgz", + "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==", + "dev": true + }, + "fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + }, + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "requires": { + "is-glob": "^4.0.3" + } + }, + "globals": { + "version": "13.21.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.21.0.tgz", + "integrity": "sha512-ybyme3s4yy/t/3s35bewwXKOf7cvzfreG2lH0lZl0JB7I4GxRP2ghxOK/Nb9EkRXdbBXZLfq/p/0W2JUONB/Gg==", + "dev": true, + "requires": { + "type-fest": "^0.20.2" + } + }, + "globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "requires": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + } + }, + "graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true + }, + "ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "requires": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true + }, + "is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "requires": { + "builtin-modules": "^3.3.0" + } + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true + }, + "is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true + }, + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + } + }, + "locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "requires": { + "p-locate": "^5.0.0" + } + }, + "lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "requires": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "natural-compare-lite": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz", + "integrity": "sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==", + "dev": true + }, + "once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "requires": { + "wrappy": "1" + } + }, + "optionator": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz", + "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==", + "dev": true, + "requires": { + "@aashutoshrathi/word-wrap": "^1.2.3", + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0" + } + }, + "p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "requires": { + "yocto-queue": "^0.1.0" + } + }, + "p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "requires": { + "p-limit": "^3.0.2" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true + }, + "path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true + }, + "punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "requires": { + "glob": "^7.1.3" + } + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "semver": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", + "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "dev": true, + "requires": { + "lru-cache": "^6.0.0" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true + }, + "spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.13", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.13.tgz", + "integrity": "sha512-XkD+zwiqXHikFZm4AX/7JSCXA98U5Db4AFd5XUg/+9UNtnH75+Z9KxtpYiJZx36mUDVOwH83pl7yvCer6ewM3w==", + "dev": true + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "requires": { + "has-flag": "^4.0.0" + } + }, + "text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "requires": { + "is-number": "^7.0.0" + } + }, + "ts-api-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.0.1.tgz", + "integrity": "sha512-lC/RGlPmwdrIBFTX59wwNzqh7aR2otPNPR/5brHZm/XKFYKsfqxihXUe9pU3JI+3vGkl+vyCoNNnPhJn3aLK1A==", + "dev": true, + "requires": {} + }, + "type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "requires": { + "prelude-ls": "^1.2.1" + } + }, + "type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true + }, + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + }, + "wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..43b0273d --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "devDependencies": { + "@gi-types/adw1": "^1.1.1", + "@gi-types/base-types": "^1.0.0", + "@gi-types/gjs-environment": "^1.1.0", + "@gi-types/gtk4-types": "^1.0.0", + "@gi-types/shell": "^0.1.6", + "@typescript-eslint/eslint-plugin": "^6.3.0", + "@typescript-eslint/parser": "^6.3.0", + "eslint": "^8.47.0", + "eslint-plugin-jsdoc": "^46.4.6", + "typescript": "^5.1.6" + } +} diff --git a/randomwallpaper@iflow.space/adapter/baseAdapter.js b/randomwallpaper@iflow.space/adapter/baseAdapter.js deleted file mode 100755 index 3863c352..00000000 --- a/randomwallpaper@iflow.space/adapter/baseAdapter.js +++ /dev/null @@ -1,126 +0,0 @@ -const Gio = imports.gi.Gio; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const LoggerModule = Self.imports.logger; -const SettingsModule = Self.imports.settings; - -/* - 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 { - constructor(params = {}) { - this.logger = new LoggerModule.Logger('RWG3', 'BaseAdapter'); - this._settings = new SettingsModule.Settings(params.schemaID, params.schemaPath); - this._wallpaperLocation = params.wallpaperLocation; - - this._sourceName = params.name; - if (this._sourceName === null || this._sourceName === "") { - this._sourceName = params.defaultName; - } - - let path = `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${params.id}/`; - this._generalSettings = new SettingsModule.Settings(SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); - } - - /** - * 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 name = this.fileName(uri); - - 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); - } - } - }); - } - - _isImageBlocked(filename) { - let blockedFilenames = this._generalSettings.get('blocked-images', 'strv'); - - if (blockedFilenames.includes(filename)) { - this.logger.warn(`Image is blocked: ${filename}`); - return true; - } - - return false; - } - - _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 deleted file mode 100644 index b7007e45..00000000 --- a/randomwallpaper@iflow.space/adapter/genericJson.js +++ /dev/null @@ -1,149 +0,0 @@ -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; - -var GenericJsonAdapter = class extends BaseAdapter.BaseAdapter { - constructor(id, name, wallpaperLocation) { - super({ - id: id, - schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON, - schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/genericJSON/${id}/`, - wallpaperLocation: wallpaperLocation, - name: name, - defaultName: 'Generic JSON Source' - }); - - this.bowl = new SoupBowl.Bowl(); - } - - _getHistoryEntry() { - return new Promise((resolve, reject) => { - let url = this._settings.get("request-url", "string"); - url = encodeURI(url); - - let message = this.bowl.Soup.Message.new('GET', url); - if (message === null) { - reject('Could not create request.'); - } - - 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 rObject; - let imageDownloadUrl; - for (let i = 0; i < 5; i++) { - rObject = JSONPath.JSONPathParser.access(response_body, imageJSONPath); - imageDownloadUrl = this._settings.get("image-prefix", "string") + rObject.Object; - - let imageBlocked = this._isImageBlocked(this.fileName(imageDownloadUrl)); - - if (!imageBlocked) { - break; - } - - // Only retry with @random present in JSONPath - if (imageBlocked && !imageJSONPath.includes("@random")) { - // Abort and try again - resolve(null); - } - - imageDownloadUrl = null; - } - - if (imageDownloadUrl === null) { - reject("Only blocked images found."); - } - - // '@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 = JSONPath.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 = JSONPath.JSONPathParser.access(response_body, authorNameJSONPath, slicedRandomElements, false).Object; - if (typeof authorName !== 'string' || !authorName instanceof String) { - authorName = null; - } - - let authorUrl = JSONPath.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; - } - - let historyEntry = new HistoryModule.HistoryEntry(authorName, this._sourceName, 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; - } - - resolve(historyEntry); - } catch (e) { - reject("Unexpected response. (" + e + ")"); - } - }); - }); - } - - async requestRandomImage(callback) { - for (let i = 0; i < 5; i++) { - try { - let historyEntry = await this._getHistoryEntry(); - - if (historyEntry === null) { - // Image blocked, try again - continue; - } - - if (callback) { - callback(historyEntry); - } - - return; - } catch (error) { - this._error(error, callback); - return; - } - } - - this._error("Only blocked images found.", 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 deleted file mode 100644 index 953baf50..00000000 --- a/randomwallpaper@iflow.space/adapter/localFolder.js +++ /dev/null @@ -1,99 +0,0 @@ -const Gio = imports.gi.Gio; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const HistoryModule = Self.imports.history; -const SettingsModule = Self.imports.settings; -const Utils = Self.imports.utils; - -const BaseAdapter = Self.imports.adapter.baseAdapter; - -var LocalFolderAdapter = class extends BaseAdapter.BaseAdapter { - constructor(id, name, wallpaperLocation) { - super({ - id: id, - schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER, - schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/localFolder/${id}/`, - wallpaperLocation: wallpaperLocation, - name: name, - defaultName: 'Local Folder' - }); - } - - 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 randomFilePath; - for (let i = 0; i < 5; i++) { - let randomFile = files[Utils.Utils.getRandomNumber(files.length)]; - randomFilePath = randomFile.get_uri(); - - if (!this._isImageBlocked(randomFile.get_basename())) { - break; - } - - randomFilePath = null; - } - - if (randomFilePath === null) { - this._error("Only blocked images found.", callback); - return; - } - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(null, this._sourceName, randomFilePath); - historyEntry.source.sourceUrl = this._wallpaperLocation; - callback(historyEntry); - } - } - - fetchFile(path, callback) { - let sourceFile = Gio.File.new_for_uri(path); - let name = 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 deleted file mode 100644 index 61773ca0..00000000 --- a/randomwallpaper@iflow.space/adapter/reddit.js +++ /dev/null @@ -1,93 +0,0 @@ -const ByteArray = imports.byteArray; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const HistoryModule = Self.imports.history; -const SettingsModule = Self.imports.settings; -const SoupBowl = Self.imports.soupBowl; -const Utils = Self.imports.utils; - -const BaseAdapter = Self.imports.adapter.baseAdapter; - -var RedditAdapter = class extends BaseAdapter.BaseAdapter { - constructor(id, name, wallpaperLocation) { - super({ - id: id, - schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_REDDIT, - schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/reddit/${id}/`, - wallpaperLocation: wallpaperLocation, - name: name, - defaultName: 'Reddit' - }); - - 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'); - - 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; - - let minWidth = this._settings.get('min-width', 'int'); - let minHeight = this._settings.get('min-height', 'int'); - if (child.data.preview.images[0].source.width < minWidth) return false; - if (child.data.preview.images[0].source.height < minHeight) return false; - - let imageRatio1 = this._settings.get('image-ratio1', 'int'); - let imageRatio2 = this._settings.get('image-ratio2', 'int'); - if (child.data.preview.images[0].source.width / imageRatio1 * imageRatio2 < child.data.preview.images[0].source.height) return false; - return true; - }); - if (submissions.length === 0) { - this._error("No suitable submissions found!", callback); - return; - } - - let submission; - let imageDownloadUrl; - for (let i = 0; i < 5; i++) { - const random = Utils.Utils.getRandomNumber(submissions.length); - submission = submissions[random].data; - imageDownloadUrl = this._ampDecode(submission.preview.images[0].source.url); - - if (!this._isImageBlocked(this.fileName(imageDownloadUrl))) { - break; - } - - imageDownloadUrl = null; - } - - if (imageDownloadUrl === null) { - this._error("Only blocked images found.", callback); - return; - } - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(null, this._sourceName, 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 deleted file mode 100644 index 71f5ce98..00000000 --- a/randomwallpaper@iflow.space/adapter/unsplash.js +++ /dev/null @@ -1,155 +0,0 @@ -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const HistoryModule = Self.imports.history; -const SettingsModule = Self.imports.settings; -const SoupBowl = Self.imports.soupBowl; -const Utils = Self.imports.utils; - -const BaseAdapter = Self.imports.adapter.baseAdapter; - -var UnsplashAdapter = class extends BaseAdapter.BaseAdapter { - constructor(id, name, wallpaperLocation) { - // Make sure we're not picking up a valid config - if (id === null) { - id = -1; - } - - super({ - id: id, - schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH, - schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/unsplash/${id}/`, - wallpaperLocation: wallpaperLocation, - name: name, - defaultName: 'Unsplash' - }); - - this._sourceUrl = 'https://source.unsplash.com'; - - // query options - this.options = { - 'query': '', - 'w': 1920, - 'h': 1080, - 'featured': false, - 'constraintType': 0, - 'constraintValue': '', - }; - - this.bowl = new SoupBowl.Bowl(); - } - - _getHistoryEntry() { - return new Promise((resolve, reject) => { - this._readOptionsFromSettings(); - let optionsString = this._generateOptionsString(); - - 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) { - reject("Could not create request."); - } - - // 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) { - reject("Unexpected response status code (expected 302)"); - } - - imageLinkUrl = message.response_headers.get_one('Location'); - - if (this._isImageBlocked(this.fileName(imageLinkUrl))) { - // Abort and try again - resolve(null); - } - - let historyEntry = new HistoryModule.HistoryEntry(null, this._sourceName, imageLinkUrl); - historyEntry.source.sourceUrl = this._sourceUrl; - historyEntry.source.imageLinkUrl = imageLinkUrl; - - resolve(historyEntry); - }); - }); - } - - async requestRandomImage(callback) { - for (let i = 0; i < 5; i++) { - try { - let historyEntry = await this._getHistoryEntry(); - - if (historyEntry === null) { - // Image blocked, try again - continue; - } - - if (callback) { - callback(historyEntry); - } - - return; - } catch (error) { - this._error(error, callback); - return; - } - } - - this._error("Only blocked images found.", callback); - } - - _generateOptionsString() { - let options = this.options; - let optionsString = ""; - - switch (options.constraintType) { - case 1: - optionsString = `/user/${options.constraintValue}/`; - break; - case 2: - optionsString = `/user/${options.constraintValue}/likes/`; - break; - case 3: - 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', 'enum'); - this.options.constraintValue = this._settings.get('constraint-value', 'string'); - - const keywords = this._settings.get('keyword', 'string').split(","); - if (keywords.length > 0) { - const randomKeyword = keywords[Utils.Utils.getRandomNumber(keywords.length)]; - this.options.query = randomKeyword.trim(); - } - - this.options.featured = this._settings.get('featured-only', 'boolean'); - } -}; diff --git a/randomwallpaper@iflow.space/adapter/urlSource.js b/randomwallpaper@iflow.space/adapter/urlSource.js deleted file mode 100644 index ba3df5f6..00000000 --- a/randomwallpaper@iflow.space/adapter/urlSource.js +++ /dev/null @@ -1,57 +0,0 @@ -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const HistoryModule = Self.imports.history; -const JSONPath = Self.imports.jsonpath.jsonpath; -const SettingsModule = Self.imports.settings; - -const BaseAdapter = Self.imports.adapter.baseAdapter; - -var UrlSourceAdapter = class extends BaseAdapter.BaseAdapter { - constructor(id, name, wallpaperLocation) { - super({ - id: id, - schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE, - schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/urlSource/${id}/`, - wallpaperLocation: wallpaperLocation, - name: name, - defaultName: 'Static URL' - }); - } - - requestRandomImage(callback) { - let imageDownloadUrl = this._settings.get("image-url", "string"); - let authorName = this._settings.get("author-name", "string"); - let authorUrl = this._settings.get("author-url", "string"); - let domainUrl = this._settings.get("domain", "string"); - let postUrl = this._settings.get("domain", "string"); - - if (typeof postUrl !== 'string' || !postUrl instanceof String) { - postUrl = null; - } - - if (typeof authorName !== 'string' || !authorName instanceof String) { - authorName = null; - } - - if (typeof authorUrl !== 'string' || !authorUrl instanceof String) { - authorUrl = null; - } - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(authorName, this._sourceName, 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); - } - } -}; diff --git a/randomwallpaper@iflow.space/adapter/wallhaven.js b/randomwallpaper@iflow.space/adapter/wallhaven.js deleted file mode 100644 index a7b884bd..00000000 --- a/randomwallpaper@iflow.space/adapter/wallhaven.js +++ /dev/null @@ -1,138 +0,0 @@ -const ByteArray = imports.byteArray; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const HistoryModule = Self.imports.history; -const SettingsModule = Self.imports.settings; -const SoupBowl = Self.imports.soupBowl; -const Utils = Self.imports.utils; - -const BaseAdapter = Self.imports.adapter.baseAdapter; - -var WallhavenAdapter = class extends BaseAdapter.BaseAdapter { - constructor(id, name, wallpaperLocation) { - super({ - id: id, - schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN, - schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/wallhaven/${id}/`, - wallpaperLocation: wallpaperLocation, - name: name, - defaultName: 'Wallhaven' - }); - - this.options = { - 'q': '', - 'apikey': '', - 'purity': '110', // SFW, sketchy - 'sorting': 'random', - 'categories': '111', // General, Anime, People - 'resolutions': ['1920x1200', '2560x1440'] - }; - - this.bowl = new SoupBowl.Bowl(); - } - - requestRandomImage(callback) { - this._readOptionsFromSettings(); - let optionsString = this._generateOptionsString(); - - 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 = null; - try { - response = JSON.parse(response_body).data; - } finally { - if (!response || response.length === 0) { - this._error("Failed to request image.", callback); - return; - } - } - - let downloadURL; - let siteURL; - for (let i = 0; i < 5; i++) { - // get a random entry from the array - let entry = response[Utils.Utils.getRandomNumber(response.length)]; - downloadURL = entry.path; - siteURL = entry.url; - - if (!this._isImageBlocked(this.fileName(downloadURL))) { - break; - } - - downloadURL = null; - } - - if (downloadURL === null) { - this._error("Only blocked images found.", callback); - return; - } - - let apiKey = this.options["apikey"]; - if (apiKey) { - downloadURL += "?apikey=" + apiKey; - } - - if (callback) { - let historyEntry = new HistoryModule.HistoryEntry(null, this._sourceName, 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[Utils.Utils.getRandomNumber(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(''); - - this.options.colors = this._settings.get('color', 'string'); - } -}; diff --git a/randomwallpaper@iflow.space/elements.js b/randomwallpaper@iflow.space/elements.js deleted file mode 100644 index 2c10983b..00000000 --- a/randomwallpaper@iflow.space/elements.js +++ /dev/null @@ -1,422 +0,0 @@ -const PopupMenu = imports.ui.popupMenu; -const St = imports.gi.St; -const Util = imports.misc.util; -const GdkPixbuf = imports.gi.GdkPixbuf; -const Clutter = imports.gi.Clutter; -const Cogl = imports.gi.Cogl; -const GLib = imports.gi.GLib; -const Gio = imports.gi.Gio; -const Gtk = imports.gi.Gtk; -const GObject = imports.gi.GObject; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const Settings = Self.imports.settings; -const LoggerModule = Self.imports.logger; -const Timer = Self.imports.timer; - -var HistoryElement = GObject.registerClass({ - GTypeName: 'HistoryElement', -}, class HistoryElement extends PopupMenu.PopupSubMenuMenuItem { - _init(historyEntry, index) { - super._init("", false); - this.logger = new LoggerModule.Logger('RWG3', 'HistoryElement'); - this.historyEntry = null; - this.setAsWallpaperItem = null; - this.previewItem = null; - this._previewActor = null; - this._settings = new Settings.Settings(); - - let timestamp = historyEntry.timestamp; - let date = new Date(timestamp); - - let timeString = date.toLocaleTimeString(); - let dateString = date.toLocaleDateString(); - - let prefixText = String(index) + '.'; - this.prefixLabel = new St.Label({ - text: prefixText, - style_class: 'rwg-history-index' - }); - - if (index === 0) { - this.label.text = 'Current Background'; - } else { - this.actor.insert_child_above(this.prefixLabel, this.label); - this.label.destroy(); - } - - this._container = new St.BoxLayout({ - vertical: true - }); - - this.dateLabel = new St.Label({ - text: dateString, - style_class: 'rwg-history-date' - }); - this._container.add_child(this.dateLabel); - - this.timeLabel = new St.Label({ - text: timeString, - style_class: 'rwg-history-time' - }); - this._container.add_child(this.timeLabel); - - this.historyEntry = historyEntry; - this.actor.historyId = historyEntry.id; // extend the actor with the historyId - - if (index !== 0) { - this.actor.insert_child_above(this._container, this.prefixLabel); - } - - this.menu.actor.add_style_class_name("rwg-history-element-content"); - - if (this.historyEntry.source !== null) { - if (this.historyEntry.source.author !== null - && this.historyEntry.source.authorUrl !== null) { - this.authorItem = new PopupMenu.PopupMenuItem('Image By: ' + this.historyEntry.source.author); - this.authorItem.connect('activate', () => { - Util.spawn(['xdg-open', this.historyEntry.source.authorUrl]); - }); - - this.menu.addMenuItem(this.authorItem); - } - - if (this.historyEntry.source.source !== null - && this.historyEntry.source.sourceUrl !== null) { - this.sourceItem = new PopupMenu.PopupMenuItem('Image From: ' + this.historyEntry.source.source); - this.sourceItem.connect('activate', () => { - Util.spawn(['xdg-open', this.historyEntry.source.sourceUrl]); - }); - - this.menu.addMenuItem(this.sourceItem); - } - - this.imageUrlItem = new PopupMenu.PopupMenuItem('Open Image In Browser'); - this.imageUrlItem.connect('activate', () => { - Util.spawn(['xdg-open', this.historyEntry.source.imageLinkUrl]); - }); - - this.menu.addMenuItem(this.imageUrlItem); - } else { - this.menu.addMenuItem(new PopupMenu.PopupMenuItem('Unknown source.')); - } - - this.previewItem = new PopupMenu.PopupBaseMenuItem({ can_focus: false, reactive: false }); - this.menu.addMenuItem(this.previewItem); - - this.setAsWallpaperItem = new PopupMenu.PopupMenuItem('Set As Wallpaper'); - this.setAsWallpaperItem.connect('activate', () => { - this.emit('activate', null); // Fixme: not sure what the second parameter should be. null seems to work fine for now. - }); - - if (index !== 0) { - // this.menu.addMenuItem(new PopupMenu.PopupBaseMenuItem({ can_focus: false, reactive: false })); // theme independent spacing - this.menu.addMenuItem(this.setAsWallpaperItem); - } - - this.copyToFavorites = new PopupMenu.PopupMenuItem('Save For Later'); - this.copyToFavorites.connect('activate', () => { - this._saveImage(); - }); - this.menu.addMenuItem(this.copyToFavorites); - - // Static URLs can't block images (yet?) - if (historyEntry.adapter.type !== 5) { - this.blockImage = new PopupMenu.PopupMenuItem('Add To Blocklist'); - this.blockImage.connect('activate', () => { - this._addToBlocklist(historyEntry); - }); - this.menu.addMenuItem(this.blockImage); - } - - /* - Load the image on first opening of the sub menu instead of during creation of the history list. - */ - this.menu.connect('open-state-changed', (self, open) => { - if (open) { - if (this._previewActor !== null) { - return; - } - - try { - let width = 270; // 270 looks good for the now fixed 350px menu width - // let width = this.menu.actor.get_width(); // This should be correct but gives different results per element? - let pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(this.historyEntry.path, width, -1, true); - let height = pixbuf.get_height(); - - let image = new Clutter.Image(); - let pixelFormat = pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888; - image.set_data( - pixbuf.get_pixels(), - pixelFormat, - width, - height, - pixbuf.get_rowstride() - ); - this._previewActor = new Clutter.Actor({ height: height, width: width }); - this._previewActor.set_content(image); - - this.previewItem.actor.add_actor(this._previewActor); - } catch (exeption) { - this.logger.error(exeption); - } - } - }) - } - - _addToBlocklist(element) { - if (element.adapter.id === null || element.adapter.id === -1) { - return; - } - - let path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${element.adapter.id}/`; - let generalSettings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); - let blockedFilenames = generalSettings.get('blocked-images', 'strv'); - - if (blockedFilenames.includes(element.name)) { - return; - } - - blockedFilenames.push(element.name); - generalSettings.set('blocked-images', 'strv', blockedFilenames); - } - - async _saveImage() { - let sourceFile = Gio.File.new_for_path(this.historyEntry.path); - let targetFolder = Gio.File.new_for_path(this._settings.get('favorites-folder', 'string')); - let targetFile = targetFolder.get_child(this.historyEntry.name); - let targetInfoFile = targetFolder.get_child(`${this.historyEntry.name}.json`); - - try { - if (!targetFolder.make_directory_with_parents(null)) { - this.logger.warn('Could not create directories.'); - return; - } - } catch (error) { - if (error === Gio.IOErrorEnum.EXISTS) { } - } - - try { // This try is for promise rejections. GJS mocks about missing this despite all examples omitting this try-catch-block - let copyResult = await new Promise((resolve, reject) => { - sourceFile.copy_async(targetFile, Gio.FileCopyFlags.NONE, GLib.PRIORITY_DEFAULT, null, null, (file, result) => { - try { - resolve(file.copy_finish(result)); - } catch (e) { - reject(e); - } - }); - }); - - if (copyResult === false) { - this.logger.warn('Failed copying image.'); - return; - } else if (copyResult === Gio.IOErrorEnum.EXISTS) { - this.logger.warn('Image already exists in location.'); - return; - } - - // https://gjs.guide/guides/gio/file-operations.html#writing-file-contents - const [, etag] = await new Promise((resolve, reject) => { - let bytes = new GLib.Bytes(JSON.stringify(this.historyEntry.source, null, '\t')); - targetInfoFile.replace_contents_bytes_async( - bytes, - null, - false, - Gio.FileCreateFlags.NONE, - null, - (file, result) => { - try { - resolve(file.replace_contents_finish(result)); - } catch (error) { - reject(error); - } - } - ); - }); - } catch (error) { - this.logger.warn(`Error saving image: ${error}`); - } - } - - setIndex(index) { - this.prefixLabel.set_text(String(index)); - } -} -); - -var CurrentImageElement = GObject.registerClass({ - GTypeName: 'CurrentImageElement', -}, class CurrentImageElement extends HistoryElement { - - _init(historyElement) { - super._init(historyElement, 0); - - if (this.setAsWallpaperItem !== null) { - this.setAsWallpaperItem.destroy(); - } - } - -}); - -/** - * Element for the New Wallpaper button and the remaining time for the auto fetch - * feature. - * The remaining time will only be displayed if the af-feature is activated. - */ -var NewWallpaperElement = GObject.registerClass({ - GTypeName: 'NewWallpaperElement', -}, class NewWallpaperElement extends PopupMenu.PopupBaseMenuItem { - - _init(params) { - super._init(params); - - this._timer = new Timer.AFTimer(); - - this._container = new St.BoxLayout({ - vertical: true - }); - - this._newWPLabel = new St.Label({ - text: 'New Wallpaper', - style_class: 'rwg-new-lable' - }); - this._container.add_child(this._newWPLabel); - - this._remainingLabel = new St.Label({ - text: '1 minute remaining' - }); - this._container.add_child(this._remainingLabel); - - this.actor.add_child(this._container); - } - - show() { - if (this._timer.isActive()) { - let remainingMinutes = this._timer.remainingMinutes(); - let minutes = remainingMinutes % 60; - let hours = Math.floor(remainingMinutes / 60); - - let hoursText = hours.toString(); - hoursText += (hours === 1) ? ' hour' : ' hours'; - let minText = minutes.toString(); - minText += (minutes === 1) ? ' minute' : ' minutes'; - - if (hours >= 1) { - this._remainingLabel.text = '... ' + hoursText + ' and ' + minText + ' remaining.' - } else { - this._remainingLabel.text = '... ' + minText + ' remaining.' - } - - this._remainingLabel.show(); - } else { - this._remainingLabel.hide(); - } - } - -}); - -var StatusElement = class { - - constructor() { - this.icon = new St.Icon({ - icon_name: 'preferences-desktop-wallpaper-symbolic', - style_class: 'system-status-icon' - }); - } - - startLoading() { - this.icon.ease({ - opacity: 20, - duration: 1337, - mode: Clutter.AnimationMode.EASE_IN_OUT_SINE, - autoReverse: true, - repeatCount: -1 - }); - } - - stopLoading() { - this.icon.remove_all_transitions(); - this.icon.opacity = 255; - } - -}; - -var HistorySection = class extends PopupMenu.PopupMenuSection { - - constructor() { - super(); - - /** - * Cache HistoryElements for performance of long histories. - */ - this._historySectionCache = {}; - - this._historyCache = []; - - this.actor = new St.ScrollView({ - hscrollbar_policy: Gtk.PolicyType.NEVER, - vscrollbar_policy: Gtk.PolicyType.AUTOMATIC - }); - - this.actor.add_actor(this.box); - } - - updateList(history, onEnter, onLeave, onSelect) { - if (this._historyCache.length <= 1) { - this.removeAll(); // remove empty history element - } - - let existingHistoryElements = []; - - for (let i = 1; i < history.length; i++) { - let historyID = history[i].id; - let tmp; - - if (!(historyID in this._historySectionCache)) { - tmp = new HistoryElement(history[i], i); - - tmp.actor.connect('key-focus-in', onEnter); - tmp.actor.connect('key-focus-out', onLeave); - tmp.actor.connect('enter-event', onEnter); - - tmp.connect('activate', onSelect); - this._historySectionCache[historyID] = tmp; - - this.addMenuItem(tmp, i - 1); - } else { - tmp = this._historySectionCache[historyID]; - tmp.setIndex(i); - } - - existingHistoryElements.push(historyID); - } - - this._cleanupHistoryCache(existingHistoryElements); - this._historyCache = history; - } - - _cleanupHistoryCache(existingIDs) { - let destroyIDs = Object.keys(this._historySectionCache).filter((i) => existingIDs.indexOf(i) === -1); - - destroyIDs.map(id => { - this._historySectionCache[id].destroy(); - delete this._historySectionCache[id]; - }); - } - - clear() { - this._cleanupHistoryCache([]); - this.removeAll(); - this.addMenuItem( - new PopupMenu.PopupMenuItem('No recent wallpaper ...', { - activate: false, - hover: false, - style_class: 'rwg-recent-lable', - can_focus: false - }) - ); - - this._historyCache = []; - } - -}; diff --git a/randomwallpaper@iflow.space/extension.js b/randomwallpaper@iflow.space/extension.js deleted file mode 100644 index 65736e4c..00000000 --- a/randomwallpaper@iflow.space/extension.js +++ /dev/null @@ -1,37 +0,0 @@ -//self -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const WallpaperController = Self.imports.wallpaperController; -const RandomWallpaperMenu = Self.imports.randomWallpaperMenu; -const LoggerModule = Self.imports.logger; - -const Timer = Self.imports.timer; - -let wallpaperController; -let panelMenu; -let logger; - -function init(metaData) { } - -function enable() { - // enable Extension - logger = new LoggerModule.Logger("RWG3", "Main"); - wallpaperController = new WallpaperController.WallpaperController(); - - logger.info("Enable extension."); - panelMenu = new RandomWallpaperMenu.RandomWallpaperMenu(wallpaperController); - panelMenu.init(); -} - -function disable() { - // disable Extension - logger.info("Disable extension."); - panelMenu.cleanup(); - - // destroy timer singleton - Timer.AFTimerDestroySingleton(); - - // clear references - wallpaperController = null; - panelMenu = null; - logger = null; -} diff --git a/randomwallpaper@iflow.space/history.js b/randomwallpaper@iflow.space/history.js deleted file mode 100644 index e345049b..00000000 --- a/randomwallpaper@iflow.space/history.js +++ /dev/null @@ -1,177 +0,0 @@ -// Filesystem -const Gio = imports.gi.Gio; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const Prefs = Self.imports.settings; -const Utils = Self.imports.utils; - -const LoggerModule = Self.imports.logger; - -var HistoryEntry = class { - - constructor(author, source, url) { - this.id = null; - this.name = null; - this.path = null; - this.source = null; - this.timestamp = new Date().getTime(); - this.adapter = { - id: null, - type: null - }; - - this.source = { - author: author, - authorUrl: null, - source: source, - sourceUrl: null, - imageDownloadUrl: url, // URL used for downloading the image - imageLinkUrl: url // URL used for linking back to the website of the image - }; - } - -}; - -var HistoryController = class { - - constructor(wallpaperLocation) { - this.logger = new LoggerModule.Logger('RWG3', 'HistoryController'); - this.size = 10; - this.history = []; - this._settings = new Prefs.Settings(); - this._wallpaperLocation = wallpaperLocation; - - this.load(); - } - - insert(historyElement) { - this.history.unshift(historyElement); - this._deleteOldPictures(); - this.save(); - } - - /** - * Set the given id to to the first history element (the current one) - * @param id - * @returns {boolean} - */ - promoteToActive(id) { - let element = this.get(id); - if (element === null) { - return false; - } - - element.timestamp = new Date().getTime(); - this.history = this.history.sort((elem1, elem2) => { - return elem1.timestamp < elem2.timestamp - }); - this.save(); - - return true; - } - - /** - * Returns the corresponding HistoryEntry or null - * @param id - * @returns {*} - */ - get(id) { - for (let elem of this.history) { - if (elem.id == id) { - return elem; - } - } - - return null; - } - - /** - * Get the current history element. - * @returns {HistoryElement} - */ - getCurrentElement() { - return this.history[0]; - } - - /** - * Get a random HistoryEntry. - * @returns {HistoryEntry} - */ - getRandom() { - return this.history[Utils.Utils.getRandomNumber(this.history.length)]; - } - - /** - * Load the history from the gschema - */ - load() { - this.size = this._settings.get('history-length', 'int'); - let stringHistory = this._settings.get('history', 'strv'); - this.history = stringHistory.map(elem => { - return JSON.parse(elem) - }); - } - - /** - * Save the history to the gschema - */ - save() { - let stringHistory = this.history.map(elem => { - return JSON.stringify(elem) - }); - this._settings.set('history', 'strv', stringHistory); - Gio.Settings.sync(); - } - - /** - * Clear the history and delete all photos except the current one. - * @returns {boolean} - */ - clear() { - let firstHistoryElement = this.history[0]; - - if (firstHistoryElement) - this.history = [firstHistoryElement]; - - let directory = Gio.file_new_for_path(this._wallpaperLocation); - let enumerator = directory.enumerate_children('', Gio.FileQueryInfoFlags.NONE, null); - - let fileinfo; - let deleteFile; - - do { - - fileinfo = enumerator.next_file(null); - - if (!fileinfo) { - break; - } - - let id = fileinfo.get_name(); - - // ignore hidden files and first element - if (id[0] != '.' && id != firstHistoryElement.id) { - deleteFile = Gio.file_new_for_path(this._wallpaperLocation + id); - deleteFile.delete(null); - } - - } while (fileinfo); - - this.save(); - return true; - } - - /** - * Delete all pictures that have no slot in the history. - * @private - */ - _deleteOldPictures() { - this.size = this._settings.get('history-length', 'int'); - let deleteFile; - while (this.history.length > this.size) { - deleteFile = Gio.file_new_for_path(this.history.pop().path); - deleteFile.delete(null); - } - } - -}; diff --git a/randomwallpaper@iflow.space/hydraPaper.js b/randomwallpaper@iflow.space/hydraPaper.js deleted file mode 100644 index 9f0423fa..00000000 --- a/randomwallpaper@iflow.space/hydraPaper.js +++ /dev/null @@ -1,88 +0,0 @@ -const Gio = imports.gi.Gio; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const Utils = Self.imports.utils; - -var HydraPaper = class { - #hydraPaperCommand = null; - #hydraPaperCancellable = null; - - constructor() { } - - /** - * Check whether HydraPaper is available on this system. - * @returns {boolean} - Whether HydraPaper is available - */ - async isAvailable() { - if (this.#hydraPaperCommand !== null) { - return true; - } - - try { - // Normal installation: - await Utils.Utils.execCheck(['hydrapaper', '--help']); - - this.#hydraPaperCommand = ['hydrapaper']; - return true; - } catch (error) { - // logError(error); - } - - try { - // FlatPak installation: - await Utils.Utils.execCheck(['org.gabmus.hydrapaper', '--help']); - - this.#hydraPaperCommand = ['org.gabmus.hydrapaper']; - return true; - } catch (error) { - // logError(error); - } - - return this.#hydraPaperCommand !== null; - } - - /** - * Cancel all running processes - * @returns - */ - cancelRunning() { - if (this.#hydraPaperCancellable === null) { - return; - } - - this.#hydraPaperCancellable.cancel(); - this.#hydraPaperCancellable = null; - } - - /** - * Generate a new combined wallpaper from multiple paths. - * - * @param {Array} wallpaperArray Array of wallpaper paths matching the monitor count - * @param {boolean} darkmode Turn on darkmode, this results into a different cache file - */ - async run(wallpaperArray, darkmode) { - // Cancel already running processes before starting new ones - this.cancelRunning(); - - // Needs a copy here - let hydraPaperCommand = [...this.#hydraPaperCommand]; - - if (darkmode) { - hydraPaperCommand.push('--darkmode'); - } - - hydraPaperCommand.push('--cli'); - hydraPaperCommand = hydraPaperCommand.concat(wallpaperArray); - - try { - this.#hydraPaperCancellable = new Gio.Cancellable(); - - // hydrapaper [--darkmode] --cli PATH PATH PATH - await Utils.Utils.execCheck(hydraPaperCommand, this.#hydraPaperCancellable); - - this.#hydraPaperCancellable = null; - } catch (error) { - logError(error); - } - } -} diff --git a/randomwallpaper@iflow.space/jsonpath/jsonpath.js b/randomwallpaper@iflow.space/jsonpath/jsonpath.js deleted file mode 100644 index 0c74385a..00000000 --- a/randomwallpaper@iflow.space/jsonpath/jsonpath.js +++ /dev/null @@ -1,114 +0,0 @@ -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const Utils = Self.imports.utils; - -var JSONPathParser = class { - - /** - * Access a simple json path expression of an object. - * Returns the accessed value or null if the access was not possible. - * - * @param inputObject the object to access - * @param inputString the json path expression - * @param randomElements the predefined random Elements - * @param newRandomness whether to ignore previously defined random Elements - * @returns {*} - */ - static access(inputObject, inputString, randomElements = null, newRandomness = true) { - if (inputObject === null || inputObject === undefined) { - return null; - } - - if (inputString.length === 0) { - return { - Object: inputObject, - RandomElements: randomElements, - }; - } - - if (randomElements === null) { - randomElements = []; - newRandomness = true; - } - - let startDot = inputString.indexOf('.'); - if (startDot === -1) { - startDot = inputString.length; - } - - let keyString = inputString.slice(0, startDot); - let inputStringTail = inputString.slice(startDot + 1); - - let startParentheses = keyString.indexOf('['); - - if (startParentheses === -1) { - - let targetObject = this._getTargetObject(inputObject, keyString); - if (targetObject == null) { - return null; - } - - return this.access(targetObject, inputStringTail, randomElements, newRandomness); - - } else { - - let indexString = keyString.slice(startParentheses + 1, keyString.length - 1); - keyString = keyString.slice(0, startParentheses); - - let targetObject = this._getTargetObject(inputObject, keyString); - if (targetObject == null) { - return null; - } - - switch (indexString) { - case "@random": - let randomNumber = null; - if (!newRandomness && randomElements.length >= 1) { - // Take and remove first element - randomNumber = randomElements.shift(); - } else if (!newRandomness && randomElements.length < 1) { - randomNumber = this.randomElement(targetObject); - } else { - randomNumber = this.randomElement(targetObject); - randomElements.push(randomNumber); - } - - return this.access(randomNumber, inputStringTail, randomElements, newRandomness); - // add special keywords here - default: - // expecting integer - return this.access(targetObject[parseInt(indexString)], inputStringTail, randomElements, newRandomness); - } - - } - - }; - - /** - * Check validity of the key string and return the target object or null. - * @param inputObject - * @param keyString - * @returns {*} - * @private - */ - static _getTargetObject(inputObject, keyString) { - if (!keyString.empty && keyString !== "$" && !inputObject.hasOwnProperty(keyString)) { - return null; - } - - return (keyString === "$") ? inputObject : inputObject[keyString]; - }; - - /** - * Returns the value of a random key of a given object. - * - * @param inputObject - * @returns {*} - */ - static randomElement(inputObject) { - let keys = Object.keys(inputObject); - let randomIndex = Utils.Utils.getRandomNumber(keys.length); - - return inputObject[keys[randomIndex]]; - } - -}; diff --git a/randomwallpaper@iflow.space/logger.js b/randomwallpaper@iflow.space/logger.js deleted file mode 100644 index 0558f675..00000000 --- a/randomwallpaper@iflow.space/logger.js +++ /dev/null @@ -1,52 +0,0 @@ -// TODO: use an enum once moved to TS -const LOG_LEVEL = { - SILENT: 0, - ERROR: 1, - WARN: 2, - INFO: 3, - DEBUG: 4, -} - -// TODO: add UI option or at least ENV variable (this is a quick workaround to conform to extension review requirements) -const CURRENT_LOG_LEVEL = LOG_LEVEL.WARN; - -var Logger = class { - - constructor(prefix, callingClass) { - this._prefix = prefix; - this._callingClass = callingClass; - } - - _log(level, message) { - log(this._prefix + ' [' + level + '] >> ' + this._callingClass + ' :: ' + message); - } - - debug(message) { - if (CURRENT_LOG_LEVEL < LOG_LEVEL.DEBUG) - return; - - this._log('DEBUG', message); - } - - info(message) { - if (CURRENT_LOG_LEVEL < LOG_LEVEL.INFO) - return; - - this._log('INFO', message); - } - - warn(message) { - if (CURRENT_LOG_LEVEL < LOG_LEVEL.WARN) - return; - - this._log('WARNING', message); - } - - error(message) { - if (CURRENT_LOG_LEVEL < LOG_LEVEL.ERROR) - return; - - this._log('ERROR', message); - } - -}; diff --git a/randomwallpaper@iflow.space/prefs.js b/randomwallpaper@iflow.space/prefs.js deleted file mode 100644 index 461778a9..00000000 --- a/randomwallpaper@iflow.space/prefs.js +++ /dev/null @@ -1,255 +0,0 @@ -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; -const Gtk = imports.gi.Gtk; -const ExtensionUtils = imports.misc.extensionUtils; - -const Self = ExtensionUtils.getCurrentExtension(); -const HydraPaper = Self.imports.hydraPaper; -const SourceRow = Self.imports.ui.sourceRow; -const Settings = Self.imports.settings; -const Utils = Self.imports.utils; -const WallpaperController = Self.imports.wallpaperController; - -const LoggerModule = Self.imports.logger; - -function init(metaData) { - //Convenience.initTranslations(); -} - -// https://gjs.guide/extensions/overview/anatomy.html#prefs-js -// The code in prefs.js will be executed in a separate Gtk process -// Here you will not have access to code running in GNOME Shell, but fatal errors or mistakes will be contained within that process. -// In this process you will be using the Gtk toolkit, not Clutter. - -// https://gjs.guide/extensions/development/preferences.html#preferences-window -// Gnome 42+ -function fillPreferencesWindow(window) { - window.set_default_size(-1, 720); - new RandomWallpaperSettings(window); -} - -// 40 < Gnome < 42 -// function buildPrefsWidget() { -// let window = new Adw.PreferencesWindow(); -// new RandomWallpaperSettings(window); -// return window; -// } - -/* UI Setup */ -var RandomWallpaperSettings = class { - constructor(window) { - this.logger = new LoggerModule.Logger('RWG3', 'RandomWallpaper.Settings'); - - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA); - this._backendConnection = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION); - this._backendConnection.set('pause-timer', 'boolean', true); - - this._sources = []; - this._loadSources(); - - this._builder = new Gtk.Builder(); - //this._builder.set_translation_domain(Self.metadata['gettext-domain']); - this._builder.add_from_file(Self.path + '/ui/pageGeneral.ui'); - this._builder.add_from_file(Self.path + '/ui/pageSources.ui'); - - this._fillTypeComboRow(); - - this._settings.bind('minutes', - this._builder.get_object('duration_minutes'), - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('hours', - this._builder.get_object('duration_hours'), - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('auto-fetch', - this._builder.get_object('af_switch'), - 'enable-expansion', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('disable-hover-preview', - this._builder.get_object('disable_hover_preview'), - 'active', - Gio.SettingsBindFlags.DEFAULT) - this._settings.bind('hide-panel-icon', - this._builder.get_object('hide_panel_icon'), - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('fetch-on-startup', - this._builder.get_object('fetch_on_startup'), - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('general-post-command', - this._builder.get_object('general_post_command'), - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('multiple-displays', - this._builder.get_object('enable_multiple_displays'), - 'active', - Gio.SettingsBindFlags.DEFAULT); - - this._bindButtons(); - this._bindHistorySection(window); - - window.connect('close-request', () => { - this._backendConnection.set('pause-timer', 'boolean', false); - }); - - window.add(this._builder.get_object('page_general')); - window.add(this._builder.get_object('page_sources')); - - this._sources.forEach(id => { - let sourceRow = new SourceRow.SourceRow(id); - this._builder.get_object('sources_list').add(sourceRow); - - sourceRow.button_delete.connect('clicked', () => { - sourceRow.clearConfig(); - this._builder.get_object('sources_list').remove(sourceRow); - Utils.Utils.removeItemOnce(this._sources, id); - this._saveSources(); - }); - }); - - try { - new HydraPaper.HydraPaper().isAvailable().then(result => { - if (result === true) { - this._builder.get_object('multiple_displays_row').set_sensitive(true); - } - }); - } catch (error) { - // Should already be handled although in a different context - } - } - - _fillTypeComboRow() { - let comboRow = this._builder.get_object('combo_background_type'); - - // Fill combo from settings enum - let availableTypes = this._settings.getSchema().get_key('change-type').get_range(); //GLib.Variant (sv) - // (sv) = Tuple(%G_VARIANT_TYPE_STRING, %G_VARIANT_TYPE_VARIANT) - // s should be 'enum' - // v should be an array enumerating the possible values. Each item in the array is a possible valid value and no other values are valid. - // v is 'as' - availableTypes = availableTypes.get_child_value(1).get_variant().get_strv(); - - let stringList = Gtk.StringList.new(availableTypes); - comboRow.model = stringList; - comboRow.selected = this._settings.get('change-type', 'enum'); - - comboRow.connect('notify::selected', comboRow => { - this._settings.set('change-type', 'enum', comboRow.selected); - }); - } - - _bindButtons() { - let newWallpaperButton = this._builder.get_object('request_new_wallpaper'); - let origNewWallpaperText = newWallpaperButton.get_child().get_label(); - newWallpaperButton.connect('activated', () => { - newWallpaperButton.get_child().set_label("Loading ..."); - newWallpaperButton.set_sensitive(false); - - // The backend sets this back to false after fetching the image - listen for that event. - let handler = this._backendConnection.observe('request-new-wallpaper', () => { - if (!this._backendConnection.get('request-new-wallpaper', 'boolean')) { - newWallpaperButton.get_child().set_label(origNewWallpaperText); - newWallpaperButton.set_sensitive(true); - this._backendConnection.disconnect(handler); - } - }); - - this._backendConnection.set('request-new-wallpaper', 'boolean', true); - }); - - let sourceRowList = this._builder.get_object('sources_list'); - this._builder.get_object('button_new_source').connect('clicked', () => { - let sourceRow = new SourceRow.SourceRow(); - sourceRowList.add(sourceRow); - this._sources.push(String(sourceRow.id)); - this._saveSources(); - - sourceRow.button_delete.connect('clicked', () => { - sourceRow.clearConfig(); - sourceRowList.remove(sourceRow); - Utils.Utils.removeItemOnce(this._sources, sourceRow.id); - this._saveSources(); - }); - }); - } - - _bindHistorySection(window) { - let entryRow = this._builder.get_object('row_favorites_folder'); - entryRow.text = this._settings.get('favorites-folder', 'string'); - - this._settings.bind('history-length', - this._builder.get_object('history_length'), - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('favorites-folder', - entryRow, - 'text', - Gio.SettingsBindFlags.DEFAULT); - - this._builder.get_object('clear_history').connect('clicked', () => { - this._backendConnection.set('clear-history', 'boolean', true); - }); - - this._builder.get_object('open_wallpaper_folder').connect('clicked', () => { - this._backendConnection.set('open-folder', 'boolean', true); - }); - - this._builder.get_object('button_favorites_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: window, - modal: true, - }); - - this._saveDialog.connect('response', (dialog, response_id) => { - if (response_id === Gtk.ResponseType.ACCEPT) { - entryRow.text = this._saveDialog.get_file().get_path(); - } - this._saveDialog.destroy(); - }); - - this._saveDialog.show(); - }); - } - - /** - * Load the config from the gschema - */ - _loadSources() { - this._sources = this._settings.get('sources', 'strv'); - - // this._sources.sort((a, b) => { - // let path1 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${a}/`; - // let settingsGeneral1 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path1); - // let path2 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${b}/`; - // let settingsGeneral2 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path2); - - // const nameA = settingsGeneral1.get('name', 'string').toUpperCase(); - // const nameB = settingsGeneral2.get('name', 'string').toUpperCase(); - - // return nameA.localeCompare(nameB); - // }); - - this._sources.sort((a, b) => { - let path1 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${a}/`; - let settingsGeneral1 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path1); - let path2 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${b}/`; - let settingsGeneral2 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path2); - return settingsGeneral1.get('type', 'enum') - settingsGeneral2.get('type', 'enum'); - }); - } - - _saveSources() { - this._settings.set('sources', 'strv', this._sources); - } -}; diff --git a/randomwallpaper@iflow.space/randomWallpaperMenu.js b/randomwallpaper@iflow.space/randomWallpaperMenu.js deleted file mode 100644 index 0c468b4b..00000000 --- a/randomwallpaper@iflow.space/randomWallpaperMenu.js +++ /dev/null @@ -1,209 +0,0 @@ -const GLib = imports.gi.GLib; -const Shell = imports.gi.Shell; - -//self -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const LoggerModule = Self.imports.logger; -const Timer = Self.imports.timer; - -// UI Imports -const PanelMenu = imports.ui.panelMenu; -const PopupMenu = imports.ui.popupMenu; -const CustomElements = Self.imports.elements; -const Main = imports.ui.main; - -// Filesystem -const Gio = imports.gi.Gio; - -// Settings -const Prefs = Self.imports.settings; - -var RandomWallpaperMenu = class { - - constructor(wallpaperController) { - this.panelMenu = new PanelMenu.Button(0, "Random wallpaper"); - this.settings = new Prefs.Settings(); - this.wallpaperController = wallpaperController; - this.logger = new LoggerModule.Logger('RWG3', 'RandomWallpaperEntry'); - this.hidePanelIconHandler = this.settings.observe('hide-panel-icon', this.updatePanelMenuVisibility.bind(this)); - this._backendConnection = new Prefs.Settings(Prefs.RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION); - - // Panelmenu Icon - this.statusIcon = new CustomElements.StatusElement(); - this.panelMenu.add_child(this.statusIcon.icon); - - // new wallpaper button - this.newWallpaperItem = new CustomElements.NewWallpaperElement(); - this.panelMenu.menu.addMenuItem(this.newWallpaperItem); - - this.panelMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - - // Set fixed width so the preview images don't widen the menu - this.panelMenu.menu.actor.set_width(350); - - // current background section - this.currentBackgroundSection = new PopupMenu.PopupMenuSection(); - this.panelMenu.menu.addMenuItem(this.currentBackgroundSection); - this.panelMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - - // history section - this.historySection = new CustomElements.HistorySection(); - this.panelMenu.menu.addMenuItem(this.historySection); - - this.panelMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); - - // Temporarily pause timer - this._pauseTimerItem = new PopupMenu.PopupSwitchMenuItem('Pause timer', false, {}); - this._pauseTimerItem.sensitive = this.settings.get('auto-fetch', 'boolean'); - this._pauseTimerItem.setToggleState(this._backendConnection.get('pause-timer', 'boolean')); - - this._pauseTimerItem.connect('toggled', (item, state) => { - this._backendConnection.set('pause-timer', 'boolean', state); - }); - - this.settings.observe('auto-fetch', () => { - this._pauseTimerItem.sensitive = this.settings.get('auto-fetch', 'boolean'); - }); - - this._backendConnection.observe('pause-timer', () => { - this._pauseTimerItem.setToggleState(this._backendConnection.get('pause-timer', 'boolean')); - }); - - this.panelMenu.menu.addMenuItem(this._pauseTimerItem); - - // clear history button - this.clearHistoryItem = new PopupMenu.PopupMenuItem('Clear History'); - this.panelMenu.menu.addMenuItem(this.clearHistoryItem); - - // open wallpaper folder button - this.openFolder = new PopupMenu.PopupMenuItem('Open Wallpaper Folder'); - this.panelMenu.menu.addMenuItem(this.openFolder); - - // settings button - this.openSettings = new PopupMenu.PopupMenuItem('Settings'); - this.panelMenu.menu.addMenuItem(this.openSettings); - - /* - add eventlistener - */ - this.wallpaperController.registerStartLoadingHook(() => this.statusIcon.startLoading()); - this.wallpaperController.registerStopLoadingHook(() => this.statusIcon.stopLoading()); - this.wallpaperController.registerStopLoadingHook(() => this.setHistoryList()); - - // new wallpaper event - this.newWallpaperItem.connect('activate', () => { - this.wallpaperController.fetchNewWallpaper(); - }); - - // clear history event - this.clearHistoryItem.connect('activate', () => { - this.wallpaperController.deleteHistory(); - }); - - // Open Wallpaper Folder - this.openFolder.connect('activate', (event) => { - let uri = GLib.filename_to_uri(this.wallpaperController.wallpaperLocation, ""); - Gio.AppInfo.launch_default_for_uri(uri, global.create_app_launch_context(0, -1)) - }); - - this.openSettings.connect("activate", () => { - // FIXME: Unhandled promise rejection. To suppress this warning, add an error handler to your promise chain with .catch() or a try-catch block around your await expression. - Gio.DBus.session.call( - 'org.gnome.Shell.Extensions', - '/org/gnome/Shell/Extensions', - 'org.gnome.Shell.Extensions', - 'OpenExtensionPrefs', - new GLib.Variant('(ssa{sv})', [Self.uuid, '', {}]), - null, - Gio.DBusCallFlags.NONE, - -1, - null); - }); - - this.panelMenu.menu.actor.connect('show', () => { - this.newWallpaperItem.show(); - }); - - // when the popupmenu disapears, check if the wallpaper is the original and - // reset it if needed - this.panelMenu.menu.actor.connect('hide', () => { - this.wallpaperController.resetWallpaper(); - }); - - this.panelMenu.menu.actor.connect('leave-event', () => { - this.wallpaperController.resetWallpaper(); - }); - - this.settings.observe('history', this.setHistoryList.bind(this)); - } - - init() { - this.updatePanelMenuVisibility(); - this.setHistoryList(); - - // add to panel - Main.panel.addToStatusArea("random-wallpaper-menu", this.panelMenu); - } - - cleanup() { - this.clearHistoryList(); - this.panelMenu.destroy(); - - // remove all signal handlers - if (this.hidePanelIconHandler !== null) { - this.settings.disconnect(this.hidePanelIconHandler); - } - } - - updatePanelMenuVisibility() { - if (this.settings.get('hide-panel-icon', 'boolean')) { - this.panelMenu.hide(); - } else { - this.panelMenu.show(); - } - } - - setCurrentBackgroundElement() { - this.currentBackgroundSection.removeAll(); - - let historyController = this.wallpaperController.getHistoryController(); - let history = historyController.history; - - if (history.length > 0) { - let currentImage = new CustomElements.CurrentImageElement(history[0]); - this.currentBackgroundSection.addMenuItem(currentImage); - } - } - - setHistoryList() { - this.wallpaperController.update(); - this.setCurrentBackgroundElement(); - - let historyController = this.wallpaperController.getHistoryController(); - let history = historyController.history; - - if (history.length <= 1) { - this.clearHistoryList(); - return; - } - - function onLeave(actor) { - this.wallpaperController.resetWallpaper(); - } - - function onEnter(actor) { - this.wallpaperController.previewWallpaper(actor.historyId); - } - - function onSelect(actor) { - this.wallpaperController.setWallpaper(actor.historyEntry.id); - } - - this.historySection.updateList(history, onEnter.bind(this), onLeave.bind(this), onSelect.bind(this)); - } - - clearHistoryList() { - this.historySection.clear(); - } - -}; diff --git a/randomwallpaper@iflow.space/schemas/.gitignore b/randomwallpaper@iflow.space/schemas/.gitignore deleted file mode 100644 index a9bbac5d..00000000 --- a/randomwallpaper@iflow.space/schemas/.gitignore +++ /dev/null @@ -1 +0,0 @@ -gschemas.compiled diff --git a/randomwallpaper@iflow.space/settings.js b/randomwallpaper@iflow.space/settings.js deleted file mode 100644 index c00da49e..00000000 --- a/randomwallpaper@iflow.space/settings.js +++ /dev/null @@ -1,59 +0,0 @@ -const Gio = imports.gi.Gio; -const Utils = imports.misc.extensionUtils; - -var RWG_SETTINGS_SCHEMA = 'org.gnome.shell.extensions.space.iflow.randomwallpaper'; -var RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.backend-connection'; -var RWG_SETTINGS_SCHEMA_SOURCES_GENERAL = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.general'; -var RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.genericJSON'; -var RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.localFolder'; -var RWG_SETTINGS_SCHEMA_SOURCES_REDDIT = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.reddit'; -var RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.unsplash'; -var RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.urlSource'; -var RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.wallhaven'; - -var RWG_SETTINGS_SCHEMA_PATH = `/org/gnome/shell/extensions/space-iflow-randomwallpaper`; - -var Settings = class { - - /** - * Settings object. - * - * @param [schema] - * @private - */ - constructor(schema, path = null) { - this._settings = Utils.getSettings(schema, path); - } - - bind(keyName, gObject, property, settingsBindFlags) { - this._settings.bind(keyName, gObject, property, settingsBindFlags); - } - - disconnect(handler) { - return this._settings.disconnect(handler); - } - - get(key, type) { - return this._settings['get_' + type](key); - } - - getSchema() { - return this._settings.settings_schema; - } - - observe(key, callback) { - return this._settings.connect('changed::' + key, callback); - } - - reset(keyName) { - this._settings.reset(keyName); - } - - set(key, type, value) { - if (this._settings['set_' + type](key, value)) { - Gio.Settings.sync(); // wait for write - } else { - throw "Could not set " + key + " (type: " + type + ") with the value " + value; - } - } -}; diff --git a/randomwallpaper@iflow.space/soupBowl.js b/randomwallpaper@iflow.space/soupBowl.js deleted file mode 100644 index ebe542e0..00000000 --- a/randomwallpaper@iflow.space/soupBowl.js +++ /dev/null @@ -1,55 +0,0 @@ -/** - * A compatibility and convenience wrapper around the Soup API. - */ -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const LoggerModule = Self.imports.logger; - -const _Soup = imports.gi.Soup; - -var Bowl = class { - - Soup = _Soup; - - constructor() { - this.logger = new LoggerModule.Logger('RWG3', 'BaseAdapter'); - - this.session = new _Soup.Session(); - - if (_Soup.get_major_version() === 2) { - this.send_and_receive = this._send_and_receive_soup24; - } else if (_Soup.get_major_version() === 3) { - this.send_and_receive = this._send_and_receive_soup30; - } else { - this.logger.error("Unknown libsoup version"); - } - } - - /* stub */ - send_and_receive(soupMessage, callback) {}; - - _send_and_receive_soup24(soupMessage, callback) { - this.session.queue_message(soupMessage, (session, msg) => { - if (!msg.response_body) { - callback(null); - return; - } - - const response_body_bytes = msg.response_body.flatten().get_data(); - callback(response_body_bytes); - }); - } - - _send_and_receive_soup30(soupMessage, callback) { - this.session.send_and_read_async(soupMessage, 0, null, (session, message) => { - const res_data = session.send_and_read_finish(message); - if (!res_data) { - callback(null); - return; - } - - const response_body_bytes = res_data.get_data(); - callback(response_body_bytes); - }); - } - -} diff --git a/randomwallpaper@iflow.space/timer.js b/randomwallpaper@iflow.space/timer.js deleted file mode 100644 index 01ec2416..00000000 --- a/randomwallpaper@iflow.space/timer.js +++ /dev/null @@ -1,153 +0,0 @@ -const GLib = imports.gi.GLib; - -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const Prefs = Self.imports.settings; -const LoggerModule = Self.imports.logger; - -let _afTimerInstance = null; - -// Singleton implementation of _AFTimer -var AFTimer = function () { - if (!_afTimerInstance) { - _afTimerInstance = new _AFTimer(); - } - return _afTimerInstance; -}; - -var AFTimerDestroySingleton = function () { - // ensure timer is removed - if (_afTimerInstance != null) { - _afTimerInstance.cleanup(); - } - - // clear reference - _afTimerInstance = null; -} - -/** - * Timer for the auto fetch feature. - */ -var _AFTimer = class { - - constructor() { - this.logger = new LoggerModule.Logger('RWG3', 'Timer'); - - this._timeout = null; - this._timoutEndCallback = null; - this._minutes = 30; - - this._settings = new Prefs.Settings(); - } - - isActive() { - return this._settings.get('auto-fetch', 'boolean'); - } - - remainingMinutes() { - let minutesElapsed = this._minutesElapsed(); - let remainder = minutesElapsed % this._minutes; - return Math.max(this._minutes - remainder, 0); - } - - registerCallback(callback) { - this._timoutEndCallback = callback; - } - - /** - * Sets the minutes of the timer. - * - * @param minutes - */ - setMinutes(minutes) { - this._minutes = minutes; - } - - /** - * Start the timer. - * - * @return void - */ - start() { - this.cleanup(); - - let last = this._settings.get('timer-last-trigger', 'int64'); - if (last === 0) { - this.reset(); - } - - let millisRemaining = this.remainingMinutes() * 60 * 1000; - - // set new wallpaper if the interval was surpassed and set the timestamp to when it should have been updated - if (this._surpassedInterval()) { - if (this._timoutEndCallback) { - this._timoutEndCallback(); - } - let millisOverdue = (this._minutes * 60 * 1000) - millisRemaining; - this._settings.set('timer-last-trigger', 'int64', Date.now() - millisOverdue); - } - - // actual timer function - this._timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, millisRemaining, () => { - if (this._timoutEndCallback) { - this._timoutEndCallback(); - } - - this.reset(); // reset timer - this.start(); // restart timer - }); - } - - /** - * Stop the timer. - * - * @return void - */ - stop() { - this._settings.set('timer-last-trigger', 'int64', 0); - this.cleanup(); - } - - /** - * Cleanup the timeout callback if it exists. - * - * @return void - */ - cleanup() { - if (this._timeout) { // only remove if a timeout is active - GLib.source_remove(this._timeout); - this._timeout = null; - } - } - - /** - * Reset the timer. - * - * @return void - */ - reset() { - this._settings.set('timer-last-trigger', 'int64', new Date().getTime()); - this.cleanup(); - } - - _minutesElapsed() { - let now = Date.now(); - let last = this._settings.get('timer-last-trigger', 'int64'); - - if (last === 0) { - return 0; - } - - let elapsed = Math.max(now - last, 0); - return Math.floor(elapsed / (60 * 1000)); - } - - _surpassedInterval() { - let now = Date.now(); - let last = this._settings.get('timer-last-trigger', 'int64'); - let diff = now - last; - let intervalLength = this._minutes * 60 * 1000; - - return diff > intervalLength; - } - -}; diff --git a/randomwallpaper@iflow.space/ui/.gitignore b/randomwallpaper@iflow.space/ui/.gitignore deleted file mode 100644 index 1a826ead..00000000 --- a/randomwallpaper@iflow.space/ui/.gitignore +++ /dev/null @@ -1 +0,0 @@ -*.ui diff --git a/randomwallpaper@iflow.space/ui/genericJson.blp b/randomwallpaper@iflow.space/ui/genericJson.blp deleted file mode 100644 index 3fdbcc52..00000000 --- a/randomwallpaper@iflow.space/ui/genericJson.blp +++ /dev/null @@ -1,107 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template GenericJsonSettingsGroup : Adw.PreferencesGroup { - // title: _("Source Settings"); - description: _("This feature requires some know how. However, many different wallpaper providers can be used with this generic JSON source.\nYou have to specify an URL to a JSON response and a path to the target image URL within the JSON response.\nYou can also define a prefix that will be added to the image URL."); - - header-suffix: LinkButton { - valign: center; - uri: "https://github.com/ifl0w/RandomWallpaperGnome3/wiki/Generic-JSON-Source"; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - }; - - Adw.PreferencesGroup { - title: _("General"); - - Adw.EntryRow domain { - title: _("Domain"); - input-purpose: url; - - LinkButton { - valign: center; - uri: bind domain.text; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - } - } - - Adw.EntryRow request_url { - title: _("Request URL"); - input-purpose: url; - - LinkButton { - valign: center; - uri: bind request_url.text; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - } - } - } - - Adw.PreferencesGroup { - title: _("Image"); - - Adw.EntryRow image_path { - title: _("JSON Path"); - input-purpose: free_form; - } - - Adw.EntryRow image_prefix { - title: _("URL prefix"); - input-purpose: free_form; - } - } - - Adw.PreferencesGroup { - title: _("Post"); - - Adw.EntryRow post_path { - title: _("JSON Path"); - input-purpose: free_form; - } - - Adw.EntryRow post_prefix { - title: _("URL Prefix"); - input-purpose: free_form; - } - } - - Adw.PreferencesGroup { - title: _("Author"); - - Adw.EntryRow author_name_path { - title: _("Name JSON Path"); - input-purpose: free_form; - } - - Adw.EntryRow author_url_path { - title: _("URL JSON Path"); - input-purpose: free_form; - } - - Adw.EntryRow author_url_prefix { - title: _("URL URL prefix"); - input-purpose: free_form; - } - } -} diff --git a/randomwallpaper@iflow.space/ui/genericJson.js b/randomwallpaper@iflow.space/ui/genericJson.js deleted file mode 100644 index 73e922b6..00000000 --- a/randomwallpaper@iflow.space/ui/genericJson.js +++ /dev/null @@ -1,80 +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 Settings = Self.imports.settings; - -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(id, params = {}) { - super(params); - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/genericJSON/${id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON, path); - - 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); - } - - clearConfig() { - this._settings.reset('domain'); - this._settings.reset('request-url'); - this._settings.reset('image-path'); - this._settings.reset('image-prefix'); - this._settings.reset('post-path'); - this._settings.reset('post-prefix'); - this._settings.reset('author-name-path'); - this._settings.reset('author-url-path'); - this._settings.reset('author-url-prefix'); - } -}); diff --git a/randomwallpaper@iflow.space/ui/localFolder.blp b/randomwallpaper@iflow.space/ui/localFolder.blp deleted file mode 100644 index 203e1f71..00000000 --- a/randomwallpaper@iflow.space/ui/localFolder.blp +++ /dev/null @@ -1,18 +0,0 @@ -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: "folder-open-symbolic"; - } - } - } -} diff --git a/randomwallpaper@iflow.space/ui/localFolder.js b/randomwallpaper@iflow.space/ui/localFolder.js deleted file mode 100644 index 353c370e..00000000 --- a/randomwallpaper@iflow.space/ui/localFolder.js +++ /dev/null @@ -1,60 +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 Gtk = imports.gi.Gtk; - -const Self = ExtensionUtils.getCurrentExtension(); -const Settings = Self.imports.settings; - -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(id, params = {}) { - super(params); - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/localFolder/${id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER, path); - - 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(); - }); - } - - clearConfig() { - this._settings.reset('folder'); - } -}); diff --git a/randomwallpaper@iflow.space/ui/pageGeneral.blp b/randomwallpaper@iflow.space/ui/pageGeneral.blp deleted file mode 100644 index f3394117..00000000 --- a/randomwallpaper@iflow.space/ui/pageGeneral.blp +++ /dev/null @@ -1,183 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -Adw.PreferencesPage page_general { - title: _("General"); - icon-name: "preferences-system-symbolic"; - - Adw.PreferencesGroup { - Adw.ActionRow request_new_wallpaper { - title: _("Request New Wallpaper"); - activatable: true; - - styles [ - "suggested-action", - "title-3", - ] - - // I don't know how to center the title so just overwrite it with a label - child: Label { - label: _("Request New Wallpaper"); - height-request: 50; - }; - } - } - - Adw.PreferencesGroup { - title: _("General Settings"); - - Adw.ComboRow combo_background_type { - title: _("Change type"); - use-subtitle: true; - } - - Adw.ActionRow { - title: _("Hide the panel icon"); - subtitle: _("You won't be able to access the history and the settings through the panel menu. Enabling this option currently is only reasonable in conjunction with the Auto-Fetching feature.\nOnly enable this option if you know how to open the settings without the panel icon!"); - - Switch hide_panel_icon { - valign: center; - } - } - - Adw.ActionRow { - title: _("Disable hover preview"); - subtitle: _("Disable the desktop preview of the background while hovering the history items. Try enabling if you encounter crashes or lags of the gnome-shell while using the extension."); - - Switch disable_hover_preview { - valign: center; - } - } - - Adw.EntryRow general_post_command { - title: _("Run post-command - available variables: %wallpaper_path%"); - } - - Adw.ActionRow multiple_displays_row { - title: _("Different wallpapers on multiple displays"); - subtitle: _("Requires HydraPaper.\nFills from History."); - sensitive: false; - - Switch enable_multiple_displays { - valign: center; - } - } - } - - Adw.PreferencesGroup { - title: _("History"); - - header-suffix: Box { - spacing: 14; - - Button open_wallpaper_folder { - Adw.ButtonContent { - icon-name: "folder-open-symbolic"; - label: _("Open"); - } - - styles [ - "flat", - ] - } - - Button clear_history { - Adw.ButtonContent { - icon-name: "user-trash-symbolic"; - label: _("Delete"); - } - - styles [ - "destructive-action", - ] - } - }; - - Adw.ActionRow { - title: _("History length"); - subtitle: _("The number of wallpapers that will be shown in the history and stored in the wallpaper folder of this extension."); - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment history_length { - lower: 1; - upper: 100; - value: 10; - step-increment: 1; - page-increment: 10; - }; - } - } - - Adw.EntryRow row_favorites_folder{ - title: _("Save for later folder"); - - Button button_favorites_folder { - valign: center; - - Adw.ButtonContent { - icon-name: "folder-open-symbolic"; - } - } - } - } - - Adw.PreferencesGroup { - title: "Auto-Fetching"; - - Adw.ExpanderRow af_switch { - title: _("Auto-Fetching"); - subtitle: _("Automatically fetch new wallpapers based on an interval."); - show-enable-switch: true; - - Adw.ActionRow { - title: _("Hours"); - - Scale duration_slider_hours { - draw-value: true; - orientation: horizontal; - hexpand: true; - digits: 0; - - adjustment: Adjustment duration_hours { - value: 1; - step-increment: 1; - page-increment: 10; - lower: 0; - upper: 23; - }; - } - } - - Adw.ActionRow { - title: _("Minutes"); - - Scale duration_slider_minutes { - draw-value: true; - orientation: horizontal; - hexpand: true; - digits: 0; - - adjustment: Adjustment duration_minutes { - value: 30; - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 59; - }; - } - } - } - - Adw.ActionRow { - title: _("Fetch on startup (experimental)"); - subtitle: _("Fetch a new wallpaper during the startup of the extension. Rebooting your system, and enabling the extension will trigger a new wallpaper request.\nWARNING: Do not enable this feature if you observe crashes when requesting new wallpapers! This could render your system unstable as crashes could repeatedly happen on startup! In the case, you encounter such a problem, you will have to disable the extension or the feature manually from the commandline for your user."); - - Switch fetch_on_startup { - valign: center; - } - } - } -} diff --git a/randomwallpaper@iflow.space/ui/pageSources.blp b/randomwallpaper@iflow.space/ui/pageSources.blp deleted file mode 100644 index d33da251..00000000 --- a/randomwallpaper@iflow.space/ui/pageSources.blp +++ /dev/null @@ -1,22 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -Adw.PreferencesPage page_sources { - title: _("Wallpaper Sources"); - icon-name: "download-symbolic"; - - Adw.PreferencesGroup sources_list { - // title: _("Configured Wallpaper Sources"); - - header-suffix: Button button_new_source { - styles [ - "suggested-action", - ] - - Adw.ButtonContent { - icon-name: "add-symbolic"; - label: _("Add Source"); - } - }; - } -} diff --git a/randomwallpaper@iflow.space/ui/reddit.blp b/randomwallpaper@iflow.space/ui/reddit.blp deleted file mode 100644 index 0ed64c54..00000000 --- a/randomwallpaper@iflow.space/ui/reddit.blp +++ /dev/null @@ -1,83 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template RedditSettingsGroup : Adw.PreferencesGroup { - title: _("General"); - - Adw.EntryRow subreddits { - title: _("Subreddits - e.g.: wallpaper, wallpapers, minimalwallpaper"); - } - - Adw.ActionRow { - title: _("Minimal resolution"); - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment min_width { - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 1000000; - }; - } - - Label { - label: "x"; - } - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment min_height { - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 1000000; - }; - } - } - - Adw.ActionRow { - title: _("Minimal image ratio"); - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment image_ratio1 { - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 1000000; - }; - } - - Label { - label: ":"; - } - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment image_ratio2 { - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 1000000; - }; - } - } - - Adw.ActionRow { - title: "SFW"; - subtitle: _("Safe for work"); - - Switch allow_sfw { - valign: center; - } - } -} diff --git a/randomwallpaper@iflow.space/ui/reddit.js b/randomwallpaper@iflow.space/ui/reddit.js deleted file mode 100644 index 3fc7624d..00000000 --- a/randomwallpaper@iflow.space/ui/reddit.js +++ /dev/null @@ -1,62 +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 Settings = Self.imports.settings; - -var RedditSettingsGroup = GObject.registerClass({ - GTypeName: 'RedditSettingsGroup', - Template: GLib.filename_to_uri(Self.path + '/ui/reddit.ui', null), - InternalChildren: [ - 'allow_sfw', - 'image_ratio1', - 'image_ratio2', - 'min_height', - 'min_width', - 'subreddits' - ] -}, class RedditSettingsGroup extends Adw.PreferencesGroup { - constructor(id, params = {}) { - super(params); - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/reddit/${id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_REDDIT, path); - - this._settings.bind('allow-sfw', - this._allow_sfw, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-ratio1', - this._image_ratio1, - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-ratio2', - this._image_ratio2, - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('min-height', - this._min_height, - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('min-width', - this._min_width, - 'value', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('subreddits', - this._subreddits, - 'text', - Gio.SettingsBindFlags.DEFAULT); - } - - clearConfig() { - this._settings.reset('allow-sfw'); - this._settings.reset('min-height'); - this._settings.reset('min-width'); - this._settings.reset('image-ratio1'); - this._settings.reset('image-ratio2'); - this._settings.reset('subreddits'); - } -}); diff --git a/randomwallpaper@iflow.space/ui/sourceRow.blp b/randomwallpaper@iflow.space/ui/sourceRow.blp deleted file mode 100644 index b6b4a0c3..00000000 --- a/randomwallpaper@iflow.space/ui/sourceRow.blp +++ /dev/null @@ -1,70 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template SourceRow : Adw.ExpanderRow { - title: bind source_name.text; - show-enable-switch: true; - - // Doesn't look good and prone to missclicks - // [action] - // Button button_delete { - // valign: center; - - // styles [ - // "destructive-action", - // ] - - // Adw.ButtonContent { - // icon-name: "user-trash-symbolic"; - // valign: center; - // } - // } - - Box { - orientation: vertical; - spacing: 14; - - Adw.Clamp { - Adw.PreferencesGroup { - title: _("Meta"); - - Adw.EntryRow source_name { - title: _("Name"); - input-purpose: free_form; - text: _("My Source - (1080p)"); - } - - Adw.ComboRow combo { - title: _("Type"); - } - - Adw.ActionRow { - title: _("Delete this source"); - - Button button_delete { - valign: center; - - styles [ - "destructive-action", - ] - - Adw.ButtonContent { - icon-name: "user-trash-symbolic"; - valign: center; - } - } - } - - Adw.ExpanderRow blocked_images_list { - title: _("Blocked Images"); - sensitive: false; - } - } - } - - Adw.Clamp settings_container { } - - // FIXME: Additional PreferencesGroup solely for spacing to the next row when expanded - Adw.PreferencesGroup { } - } -} diff --git a/randomwallpaper@iflow.space/ui/sourceRow.js b/randomwallpaper@iflow.space/ui/sourceRow.js deleted file mode 100644 index 9a107612..00000000 --- a/randomwallpaper@iflow.space/ui/sourceRow.js +++ /dev/null @@ -1,165 +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 Gtk = imports.gi.Gtk; - -const Self = ExtensionUtils.getCurrentExtension(); -const Settings = Self.imports.settings; -const Utils = Self.imports.utils; - -const GenericJson = Self.imports.ui.genericJson; -const LocalFolder = Self.imports.ui.localFolder; -const Reddit = Self.imports.ui.reddit; -const Unsplash = Self.imports.ui.unsplash; -const UrlSource = Self.imports.ui.urlSource; -const Wallhaven = Self.imports.ui.wallhaven; - -// 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' - ], - InternalChildren: [ - 'blocked_images_list', - 'combo', - 'settings_container', - 'source_name' - ] -}, class SourceRow extends Adw.ExpanderRow { - // This list is the same across all rows - static _stringList = null; - - constructor(id = null, params = {}) { - super(params); - - if (id === null) { - // New row - this.id = Date.now(); - } else { - this.id = id; - } - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${this.id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); - - if (this._stringList === null || this._stringList === undefined) { - // Fill combo from settings enum - - let availableTypes = this._settings.getSchema().get_key('type').get_range(); //GLib.Variant (sv) - // (sv) = Tuple(%G_VARIANT_TYPE_STRING, %G_VARIANT_TYPE_VARIANT) - // s should be 'enum' - // v should be an array enumerating the possible values. Each item in the array is a possible valid value and no other values are valid. - // v is 'as' - availableTypes = availableTypes.get_child_value(1).get_variant().get_strv(); - - this._stringList = Gtk.StringList.new(availableTypes); - } - this._combo.model = this._stringList; - this._combo.selected = this._settings.get('type', 'enum'); - - this._settings.bind('name', - this._source_name, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('enabled', - this, - 'enable-expansion', - Gio.SettingsBindFlags.DEFAULT); - // Binding an enum isn't possible straight away. - // This would need bind_with_mapping() which isn't available in gjs? - // this._settings.bind('type', - // this._combo, - // 'selected', - // Gio.SettingsBindFlags.DEFAULT); - - this._combo.connect('notify::selected', comboRow => { - this._settings.set('type', 'enum', comboRow.selected); - this._fillRow(comboRow.selected); - }); - - this._fillRow(this._combo.selected); - - let blockedImages = this._settings.get('blocked-images', 'strv'); - blockedImages.forEach(filename => { - let blockedImageRow = new Adw.ActionRow(); - blockedImageRow.set_title(filename); - - let button = new Gtk.Button(); - button.set_valign(Gtk.Align.CENTER); - button.connect('clicked', () => { - this._removeBlockedImage(filename); - this._blocked_images_list.remove(blockedImageRow); - }); - - let buttonContent = new Adw.ButtonContent(); - buttonContent.set_icon_name("user-trash-symbolic") - - button.set_child(buttonContent); - blockedImageRow.add_suffix(button); - this._blocked_images_list.add_row(blockedImageRow); - this._blocked_images_list.set_sensitive(true); - }); - } - - _fillRow(type) { - let targetWidget = this._getSettingsGroup(type); - if (targetWidget !== null) { - this._settings_container.set_child(targetWidget); - } - } - - _getSettingsGroup(type = 0) { - let targetWidget; - switch (type) { - case 0: // unsplash - targetWidget = new Unsplash.UnsplashSettingsGroup(this.id); - break; - case 1: // wallhaven - targetWidget = new Wallhaven.WallhavenSettingsGroup(this.id); - break; - case 2: // reddit - targetWidget = new Reddit.RedditSettingsGroup(this.id); - break; - case 3: // generic JSON - targetWidget = new GenericJson.GenericJsonSettingsGroup(this.id); - break; - case 4: // Local Folder - targetWidget = new LocalFolder.LocalFolderSettingsGroup(this.id); - break; - case 5: // Static URL - targetWidget = new UrlSource.UrlSourceSettingsGroup(this.id); - break; - default: - targetWidget = null; - this.logger.error("The selected source has no corresponding widget!") - break; - } - return targetWidget; - } - - _removeBlockedImage(filename) { - let blockedImages = this._settings.get('blocked-images', 'strv'); - if (!blockedImages.includes(filename)) { - return; - } - - blockedImages = Utils.Utils.removeItemOnce(blockedImages, filename); - this._settings.set('blocked-images', 'strv', blockedImages); - } - - clearConfig() { - for (const i of Array(6).keys()) { - let widget = this._getSettingsGroup(i); - widget.clearConfig(); - } - - this._settings.reset('blocked-images'); - this._settings.reset('enabled'); - this._settings.reset('name'); - this._settings.reset('type'); - } -}); diff --git a/randomwallpaper@iflow.space/ui/unsplash.blp b/randomwallpaper@iflow.space/ui/unsplash.blp deleted file mode 100644 index de5ff4dc..00000000 --- a/randomwallpaper@iflow.space/ui/unsplash.blp +++ /dev/null @@ -1,66 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template UnsplashSettingsGroup : Adw.PreferencesGroup { - title: _("General"); - - Adw.EntryRow keyword { - title: _("Keywords - Comma seperated"); - input-purpose: free_form; - } - - Adw.ActionRow { - title: _("Only Featured Images"); - subtitle: _("This option results in a smaller image pool, but the images are considered to be of higher quality."); - - Switch featured_only { - valign: center; - } - } - - Adw.ActionRow { - title: _("Image Dimensions"); - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment image_width { - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 1000000; - }; - } - - Label { - label: "x"; - } - - SpinButton { - valign: center; - numeric: true; - - adjustment: Adjustment image_height { - step-increment: 1; - page-increment: 10; - lower: 1; - upper: 1000000; - }; - } - } - - Adw.PreferencesGroup { - title: _("Contraint"); - - // TODO: ExpanderRow with switch? - // Nested ExpanderRows behave strangely - Adw.ComboRow constraint_type { - title: _("Type"); - } - - Adw.EntryRow constraint_value { - title: _("Value"); - } - } -} diff --git a/randomwallpaper@iflow.space/ui/unsplash.js b/randomwallpaper@iflow.space/ui/unsplash.js deleted file mode 100644 index b3807325..00000000 --- a/randomwallpaper@iflow.space/ui/unsplash.js +++ /dev/null @@ -1,102 +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 Gtk = imports.gi.Gtk; - -const Self = ExtensionUtils.getCurrentExtension(); -const Settings = Self.imports.settings; - -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' - ] -}, class UnsplashSettingsGroup extends Adw.PreferencesGroup { - // This list is the same across all rows - static _stringList = null; - - constructor(id, params = {}) { - super(params); - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/unsplash/${id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH, path); - - if (this._stringList === null || this._stringList === undefined) { - // Fill combo from settings enum - - let availableTypes = this._settings.getSchema().get_key('constraint-type').get_range(); //GLib.Variant (sv) - // (sv) = Tuple(%G_VARIANT_TYPE_STRING, %G_VARIANT_TYPE_VARIANT) - // s should be 'enum' - // v should be an array enumerating the possible values. Each item in the array is a possible valid value and no other values are valid. - // v is 'as' - availableTypes = availableTypes.get_child_value(1).get_variant().get_strv(); - - this._stringList = Gtk.StringList.new(availableTypes); - } - - this._constraint_type.model = this._stringList; - this._constraint_type.selected = this._settings.get('constraint-type', 'enum'); - - 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); - // Binding an enum isn't possible straight away. - // This would need bind_with_mapping() which isn't available in gjs? - // 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._settings.set('constraint-type', 'enum', comboRow.selected); - - this._featured_only.set_active(false); - }); - } - - _unsplashUnconstrained(comboRow, enable, targetElement) { - if (comboRow.selected === 0) { - targetElement.set_sensitive(enable); - } else { - targetElement.set_sensitive(!enable); - } - } - - clearConfig() { - this._settings.reset('keyword'); - this._settings.reset('image-width'); - this._settings.reset('image-height'); - this._settings.reset('featured-only'); - this._settings.reset('constraint-type'); - this._settings.reset('constraint-value'); - } -}); diff --git a/randomwallpaper@iflow.space/ui/urlSource.blp b/randomwallpaper@iflow.space/ui/urlSource.blp deleted file mode 100644 index f1aff9fa..00000000 --- a/randomwallpaper@iflow.space/ui/urlSource.blp +++ /dev/null @@ -1,75 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template UrlSourceSettingsGroup : Adw.PreferencesGroup { - Adw.PreferencesGroup { - title: _("General"); - - Adw.EntryRow domain { - title: _("Domain"); - input-purpose: url; - - LinkButton { - valign: center; - uri: bind domain.text; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - } - } - - Adw.EntryRow image_url { - title: _("Image URL"); - - LinkButton { - valign: center; - uri: bind image_url.text; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - } - } - - Adw.EntryRow post_url { - title: _("Post URL"); - input-purpose: free_form; - - LinkButton { - valign: center; - uri: bind post_url.text; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - } - } - } - - Adw.PreferencesGroup { - title: _("Author"); - - Adw.EntryRow author_name { - title: _("Name"); - input-purpose: free_form; - } - - Adw.EntryRow author_url { - title: _("URL"); - input-purpose: free_form; - } - } -} diff --git a/randomwallpaper@iflow.space/ui/urlSource.js b/randomwallpaper@iflow.space/ui/urlSource.js deleted file mode 100644 index 05e95aca..00000000 --- a/randomwallpaper@iflow.space/ui/urlSource.js +++ /dev/null @@ -1,56 +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 Settings = Self.imports.settings; - -var UrlSourceSettingsGroup = GObject.registerClass({ - GTypeName: 'UrlSourceSettingsGroup', - Template: GLib.filename_to_uri(Self.path + '/ui/urlSource.ui', null), - InternalChildren: [ - 'author_name', - 'author_url', - 'domain', - 'image_url', - 'post_url', - ] -}, class UrlSourceSettingsGroup extends Adw.PreferencesGroup { - constructor(id, params = {}) { - super(params); - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/urlSource/${id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE, path); - - this._settings.bind('author-name', - this._author_name, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('author-url', - this._author_url, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('domain', - this._domain, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('image-url', - this._image_url, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('post-url', - this._post_url, - 'text', - Gio.SettingsBindFlags.DEFAULT); - } - - clearConfig() { - this._settings.reset('author-name'); - this._settings.reset('author-url'); - this._settings.reset('domain'); - this._settings.reset('image-url'); - this._settings.reset('post-url'); - } -}); diff --git a/randomwallpaper@iflow.space/ui/wallhaven.blp b/randomwallpaper@iflow.space/ui/wallhaven.blp deleted file mode 100644 index 0ba3f901..00000000 --- a/randomwallpaper@iflow.space/ui/wallhaven.blp +++ /dev/null @@ -1,124 +0,0 @@ -using Gtk 4.0; -using Adw 1; - -template WallhavenSettingsGroup : Adw.PreferencesGroup { - // title: _("Source Settings"); - - Adw.PreferencesGroup { - title: _("General"); - - Adw.EntryRow keyword { - title: _("Keywords - Comma seperated"); - input-purpose: free_form; - } - - Adw.PasswordEntryRow api_key { - title: _("API key"); - input-purpose: password; - - LinkButton { - valign: center; - uri: "https://wallhaven.cc/settings/account"; - - Adw.ButtonContent { - icon-name: "globe-symbolic"; - } - - styles [ - "flat", - ] - } - } - - Adw.EntryRow resolutions { - title: _("Resolutions: 1920x1080, 2560x1440"); - input-purpose: free_form; - text: ""; - } - - Adw.ActionRow row_color { - title: _("Search by color"); - subtitle: ""; - - Box { - Button button_color_undo { - valign: center; - - styles [ - "flat", - ] - - Adw.ButtonContent { - icon-name: "edit-undo-symbolic"; - } - } - - Button button_color { - valign: center; - - Adw.ButtonContent { - icon-name: "color-select-symbolic"; - } - } - } - } - } - - Adw.PreferencesGroup { - title: _("Allowed content ratings"); - - Adw.ActionRow { - title: "SFW"; - subtitle: _("Safe for work"); - - Switch allow_sfw { - valign: center; - } - } - - Adw.ActionRow { - title: "Sketchy"; - - Switch allow_sketchy { - valign: center; - } - } - - Adw.ActionRow { - title: "NSFW"; - subtitle: _("Not safe for work"); - - Switch allow_nsfw { - valign: center; - } - } - } - - Adw.PreferencesGroup { - title: _("Categories"); - - Adw.ActionRow { - title: "General"; - - Switch category_general { - valign: center; - } - } - - Adw.ActionRow { - title: "Anime"; - - Switch category_anime { - valign: center; - } - } - - Adw.ActionRow { - title: "People"; - - Switch category_people { - valign: center; - } - } - } -} diff --git a/randomwallpaper@iflow.space/ui/wallhaven.js b/randomwallpaper@iflow.space/ui/wallhaven.js deleted file mode 100644 index d73f38f9..00000000 --- a/randomwallpaper@iflow.space/ui/wallhaven.js +++ /dev/null @@ -1,140 +0,0 @@ -const Adw = imports.gi.Adw; -const ExtensionUtils = imports.misc.extensionUtils; -const Gdk = imports.gi.Gdk; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; -const GObject = imports.gi.GObject; -const Gtk = imports.gi.Gtk; - -const Self = ExtensionUtils.getCurrentExtension(); -const Settings = Self.imports.settings; - -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', - 'button_color', - 'button_color_undo', - 'category_anime', - 'category_general', - 'category_people', - 'keyword', - 'resolutions', - 'row_color' - ] -}, class WallhavenSettingsGroup extends Adw.PreferencesGroup { - constructor(id, params = {}) { - super(params); - - const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/wallhaven/${id}/`; - this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN, path); - - this._settings.bind('allow-nsfw', - this._allow_nsfw, - '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('api-key', - this._api_key, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('category-anime', - this._category_anime, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('category-general', - this._category_general, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('category-people', - this._category_people, - 'active', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('color', - this._row_color, - 'subtitle', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('keyword', - this._keyword, - 'text', - Gio.SettingsBindFlags.DEFAULT); - this._settings.bind('resolutions', - this._resolutions, - 'text', - Gio.SettingsBindFlags.DEFAULT); - - this._button_color_undo.connect('clicked', () => { - this._row_color.subtitle = ""; - }); - - const availableColors = [ - "#660000", "#990000", "#cc0000", "#cc3333", "#ea4c88", - "#993399", "#663399", "#333399", "#0066cc", "#0099cc", - "#66cccc", "#77cc33", "#669900", "#336600", "#666600", - "#999900", "#cccc33", "#ffff00", "#ffcc33", "#ff9900", - "#ff6600", "#cc6633", "#996633", "#663300", "#000000", - "#999999", "#cccccc", "#ffffff", "#424153", - ]; - - this._colorPalette = []; - - availableColors.forEach(hexColor => { - let rgbaColor = new Gdk.RGBA(); - rgbaColor.parse(hexColor); - this._colorPalette.push(rgbaColor); - }); - - this._button_color.connect('clicked', () => { - // For GTK 4.10+ - // Gtk.ColorDialog(); - - // https://stackoverflow.com/a/54487948 - this._colorDialog = new Gtk.ColorChooserDialog({ - title: 'Choose a Color', - transient_for: this.get_root(), - modal: true, - }); - this._colorDialog.set_use_alpha(false); - this._colorDialog.add_palette(Gtk.Orientation.HORIZONTAL, 10, this._colorPalette); - - this._colorDialog.connect('response', (dialog, response_id) => { - if (response_id === Gtk.ResponseType.OK) { - // result is a Gdk.RGBA which uses float - let rgba = this._colorDialog.get_rgba(); - // convert to rgba so it's useful - let rgbaString = rgba.to_string(); // rgb(0,0,0) - let rgbaArray = rgbaString.replace("rgb(", "").replace(")", "").split(",") - let hexString = `${parseInt(rgbaArray[0]).toString(16).padStart(2, "0")}${parseInt(rgbaArray[1]).toString(16).padStart(2, "0")}${parseInt(rgbaArray[2]).toString(16).padStart(2, "0")}`; - this._row_color.subtitle = hexString; - } - this._colorDialog.destroy(); - }); - - this._colorDialog.show(); - }); - } - - clearConfig() { - this._settings.reset('allow-nsfw'); - this._settings.reset('allow-sfw'); - this._settings.reset('allow-sketchy'); - this._settings.reset('api-key'); - this._settings.reset('category-anime'); - this._settings.reset('category-general'); - this._settings.reset('category-people'); - this._settings.reset('color'); - this._settings.reset('keyword'); - this._settings.reset('resolutions'); - } -}); diff --git a/randomwallpaper@iflow.space/utils.js b/randomwallpaper@iflow.space/utils.js deleted file mode 100644 index 79c9cbe0..00000000 --- a/randomwallpaper@iflow.space/utils.js +++ /dev/null @@ -1,84 +0,0 @@ -const Gdk = imports.gi.Gdk; -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; - -var Utils = class { - /** - * Get the monitor count for the default "seat". - * @returns Number - */ - static getMonitorCount() { - // Gdk 4.8+ - // Gdk.DisplayManager.get() - // displayManager.get_default_display() - // display.get_monitors() - // monitors.get_n_items() <- Monitor count, number - - // let defaultDisplay = Gdk.Display.get_default(); // default "seat" which can have multiple monitors - // let monitorList = defaultDisplay.get_monitors(); // Gio.ListModel containing all "Gdk.Monitor" - // return monitorList.get_n_items(); - - // Gdk < 4.8 - let defaultDisplay = Gdk.Display.get_default(); // default "seat" which can have multiple monitors - return defaultDisplay.get_n_monitors(); - } - - static getRandomNumber(size) { - return Math.floor(Math.random() * size); - } - - // https://gjs.guide/guides/gio/subprocesses.html#complete-examples - /** - * Execute a command asynchronously and check the exit status. - * - * If given, @cancellable can be used to stop the process before it finishes. - * - * @param {string[]} argv - a list of string arguments - * @param {Gio.Cancellable} [cancellable] - optional cancellable object - * @returns {Promise<>} - The process success - */ - static async execCheck(argv, cancellable = null) { - let cancelId = 0; - let proc = new Gio.Subprocess({ - argv: argv, - flags: Gio.SubprocessFlags.NONE - }); - proc.init(cancellable); - - if (cancellable instanceof Gio.Cancellable) { - cancelId = cancellable.connect(() => proc.force_exit()); - } - - return new Promise((resolve, reject) => { - proc.wait_check_async(null, (proc, res) => { - try { - if (!proc.wait_check_finish(res)) { - let status = proc.get_exit_status(); - - throw new Gio.IOErrorEnum({ - code: Gio.io_error_from_errno(status), - message: GLib.strerror(status) - }); - } - - resolve(); - } catch (e) { - reject(e); - } finally { - if (cancelId > 0) { - cancellable.disconnect(cancelId); - } - } - }); - }); - } - - // https://stackoverflow.com/a/5767357 - static removeItemOnce(arr, value) { - var index = arr.indexOf(value); - if (index > -1) { - arr.splice(index, 1); - } - return arr; - } -} diff --git a/randomwallpaper@iflow.space/wallpaperController.js b/randomwallpaper@iflow.space/wallpaperController.js deleted file mode 100644 index bee43d50..00000000 --- a/randomwallpaper@iflow.space/wallpaperController.js +++ /dev/null @@ -1,569 +0,0 @@ -const Mainloop = imports.gi.GLib; - -// Filesystem -const Gio = imports.gi.Gio; -const GLib = imports.gi.GLib; - -//self -const Self = imports.misc.extensionUtils.getCurrentExtension(); -const HistoryModule = Self.imports.history; -const HydraPaper = Self.imports.hydraPaper; -const LoggerModule = Self.imports.logger; -const Prefs = Self.imports.settings; -const Utils = Self.imports.utils; -const Timer = Self.imports.timer; - -// 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 UrlSourceAdapter = Self.imports.adapter.urlSource; -const WallhavenAdapter = Self.imports.adapter.wallhaven; - -var WallpaperController = class { - _backendConnection = null; - _prohibitTimer = false; - - constructor() { - this.logger = new LoggerModule.Logger('RWG3', 'WallpaperController'); - let xdg_cache_home = Mainloop.getenv('XDG_CACHE_HOME') - if (!xdg_cache_home) { - xdg_cache_home = `${Mainloop.getenv('HOME')}/.cache` - } - this.wallpaperLocation = `${xdg_cache_home}/${Self.metadata['uuid']}/wallpapers/`; - let mode = parseInt('0755', 8); - Mainloop.mkdir_with_parents(this.wallpaperLocation, mode) - - this._autoFetch = { - active: false, - duration: 30, - }; - - // functions will be called upon loading a new wallpaper - this._startLoadingHooks = []; - // functions will be called when loading a new wallpaper stopped. If an error occurred then the error will be passed as parameter. - this._stopLoadingHooks = []; - - this._backendConnection = new Prefs.Settings(Prefs.RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION); - - // Bring values to defined stage - this._backendConnection.set('clear-history', 'boolean', false); - this._backendConnection.set('open-folder', 'boolean', false); - this._backendConnection.set('pause-timer', 'boolean', false); - this._backendConnection.set('request-new-wallpaper', 'boolean', false); - - // Track value changes - this._backendConnection.observe('clear-history', () => this._clearHistory()); - this._backendConnection.observe('open-folder', () => this._openFolder()); - this._backendConnection.observe('pause-timer', () => this._pauseTimer()); - this._backendConnection.observe('request-new-wallpaper', () => this._requestNewWallpaper()); - - this._timer = new Timer.AFTimer(); - this._historyController = new HistoryModule.HistoryController(this.wallpaperLocation); - this._hydraPaper = new HydraPaper.HydraPaper(); - - this._settings = new Prefs.Settings(); - this._settings.observe('history-length', () => this._updateHistory()); - this._settings.observe('auto-fetch', () => this._updateAutoFetching()); - this._settings.observe('minutes', () => this._updateAutoFetching()); - this._settings.observe('hours', () => this._updateAutoFetching()); - - this._updateHistory(); - this._updateAutoFetching(); - - // load a new wallpaper on startup - if (this._settings.get("fetch-on-startup", "boolean")) { - this.fetchNewWallpaper(); - } - - // Initialize favorites folder - // TODO: There's probably a better place for this - let favoritesFolder = this._settings.get('favorites-folder', 'string'); - if (favoritesFolder === "") { - const directoryPictures = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); - - if (directoryPictures === null) { - // Pictures not set up - const directoryDownloads = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOWNLOAD); - - if (directoryDownloads === null) { - const xdg_data_home = GLib.get_user_data_dir(); - favoritesFolder = Gio.File.new_for_path(xdg_data_home); - } else { - favoritesFolder = Gio.File.new_for_path(directoryDownloads); - } - } else { - favoritesFolder = Gio.File.new_for_path(directoryPictures); - } - - favoritesFolder = favoritesFolder.get_child(Self.metadata['uuid']); - - this._settings.set('favorites-folder', 'string', favoritesFolder.get_path()); - } - } - - _clearHistory() { - if (this._backendConnection.get('clear-history', 'boolean')) { - this.update(); - this.deleteHistory(); - this._backendConnection.set('clear-history', 'boolean', false); - } - } - - _openFolder() { - if (this._backendConnection.get('open-folder', 'boolean')) { - let uri = GLib.filename_to_uri(this.wallpaperLocation, ""); - Gio.AppInfo.launch_default_for_uri(uri, Gio.AppLaunchContext.new()); - this._backendConnection.set('open-folder', 'boolean', false); - } - } - - _pauseTimer() { - if (this._backendConnection.get('pause-timer', 'boolean')) { - this._prohibitTimer = true; - this._updateAutoFetching(); - } else { - this._prohibitTimer = false; - this._updateAutoFetching(); - } - } - - _requestNewWallpaper() { - if (this._backendConnection.get('request-new-wallpaper', 'boolean')) { - this.update(); - this.fetchNewWallpaper(() => { - this.update(); - this._backendConnection.set('request-new-wallpaper', 'boolean', false); - }); - } - } - - _updateHistory() { - this._historyController.load(); - } - - _updateAutoFetching() { - let duration = 0; - duration += this._settings.get('minutes', 'int'); - duration += this._settings.get('hours', 'int') * 60; - this._autoFetch.duration = duration; - this._autoFetch.active = this._settings.get('auto-fetch', 'boolean'); - - // only start timer if not in context of preferences window - if (!this._prohibitTimer && this._autoFetch.active) { - this._timer.registerCallback(() => this.fetchNewWallpaper()); - this._timer.setMinutes(this._autoFetch.duration); - this._timer.start(); - } else { - this._timer.stop(); - } - } - - /* - randomly returns an enabled and configured SourceAdapter - returns a default UnsplashAdapter in case of failure - */ - _getRandomAdapter() { - let imageSourceAdapter = null; - let sourceID = this._getRandomSource(); - - let path = `${Prefs.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${sourceID}/`; - let settingsGeneral = new Prefs.Settings(Prefs.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); - - let sourceName = settingsGeneral.get('name', 'string'); - let sourceType = settingsGeneral.get('type', 'enum'); - - if (sourceID === -1) { - sourceType = null; - } - - try { - switch (sourceType) { - case 0: - imageSourceAdapter = new UnsplashAdapter.UnsplashAdapter(sourceID, sourceName, this.wallpaperLocation); - break; - case 1: - imageSourceAdapter = new WallhavenAdapter.WallhavenAdapter(sourceID, sourceName, this.wallpaperLocation); - break; - case 2: - imageSourceAdapter = new RedditAdapter.RedditAdapter(sourceID, sourceName, this.wallpaperLocation); - break; - case 3: - imageSourceAdapter = new GenericJsonAdapter.GenericJsonAdapter(sourceID, sourceName, this.wallpaperLocation); - break; - case 4: - imageSourceAdapter = new LocalFolderAdapter.LocalFolderAdapter(sourceID, sourceName, this.wallpaperLocation); - break; - case 5: - imageSourceAdapter = new UrlSourceAdapter.UrlSourceAdapter(sourceID, sourceName, this.wallpaperLocation); - break; - default: - imageSourceAdapter = new UnsplashAdapter.UnsplashAdapter(null, null, this.wallpaperLocation); - sourceType = 0; - break; - } - } catch (error) { - this.logger.warn("Had errors, fetching with default settings."); - imageSourceAdapter = new UnsplashAdapter.UnsplashAdapter(null, null, this.wallpaperLocation); - sourceType = 0; - } - - return { - adapter: imageSourceAdapter, - adapterId: sourceID, - adapterType: sourceType - }; - } - - _getRandomSource() { - let sources = this._settings.get('sources', 'strv'); - - if (sources === null || sources.length < 1) { - return -1; - } - - let enabled_sources = sources.filter(element => { - let path = `${Prefs.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${element}/`; - let settingsGeneral = new Prefs.Settings(Prefs.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); - return settingsGeneral.get('enabled', 'boolean'); - }); - - if (enabled_sources === null || enabled_sources.length < 1) { - return -1; - } - - // https://stackoverflow.com/a/5915122 - return enabled_sources[Utils.Utils.getRandomNumber(enabled_sources.length)]; - } - - /** - * Sets the wallpaper and the lockscreen when enabled to the given path. Executes the callback on success. - * @param path - * @param callback - * @private - */ - async _setBackground(path, callback) { - let monitorCount = Utils.Utils.getMonitorCount(); - let background_setting = new Gio.Settings({ schema: "org.gnome.desktop.background" }); - let screensaver_setting = new Gio.Settings({ schema: "org.gnome.desktop.screensaver" }); - let wallpaperUri = "file://" + path; - - let changeType = this._settings.get('change-type', 'enum'); - // - // - // - // TODO: - - if (changeType === 0 || changeType === 2) { - try { - if (this._settings.get('multiple-displays', 'boolean') && this._hydraPaper !== null && await this._hydraPaper.isAvailable()) { - let wallpaperArray = [path]; - - // Abuse history to fill missing images - for (let index = 0; index < monitorCount - 1; index++) { - let historyElement; - do { - historyElement = this._historyController.getRandom(); - } while (this._historyController.history.length > monitorCount && wallpaperArray.includes(historyElement.path, 1)) - // ensure different wallpaper for all displays if possible - - wallpaperArray.push(historyElement.path); - } - - await this._hydraPaper.run(wallpaperArray); - - // Manually set key for darkmode because that's way faster - background_setting.set_string("picture-uri-dark", background_setting.get_string("picture-uri")); - - Gio.Settings.sync(); - } else { - // set "picture-options" to "zoom" for single wallpapers - // hydrapaper changes this to "spanned" - background_setting.set_string('picture-options', 'zoom'); - this._setPictureUriOfSettingsObject(background_setting, wallpaperUri); - } - } catch (error) { - this.logger.warn(error); - } - } - - if (changeType === 1) { - try { - if (this._settings.get('multiple-displays', 'boolean') && this._hydraPaper !== null && await this._hydraPaper.isAvailable()) { - let wallpaperArray = [path]; - - // Abuse history to fill missing images - for (let index = 0; index < monitorCount - 1; index++) { - let historyElement; - do { - historyElement = this._historyController.getRandom(); - } while (this._historyController.history.length > monitorCount && wallpaperArray.includes(historyElement.path, 1)) - // ensure different wallpaper for all displays if possible - - wallpaperArray.push(historyElement.path); - } - - // Remember keys, HydraPaper will change these - let tmpBackground = background_setting.get_string("picture-uri-dark"); - let tmpMode = background_setting.get_string("picture-options"); - - // Force HydraPaper to target a different resulting image by using darkmode - await this._hydraPaper.run(wallpaperArray, true); - - screensaver_setting.set_string("picture-options", "spanned"); - this._setPictureUriOfSettingsObject(screensaver_setting, background_setting.get_string("picture-uri-dark")) - - // HydraPaper possibly changed these, change them back - background_setting.set_string("picture-uri-dark", tmpBackground); - background_setting.set_string("picture-options", tmpMode); - - Gio.Settings.sync(); - } else { - // set "picture-options" to "zoom" for single wallpapers - screensaver_setting.set_string('picture-options', 'zoom'); - this._setPictureUriOfSettingsObject(screensaver_setting, wallpaperUri); - } - } catch (error) { - this.logger.warn(error); - } - } - - if (changeType === 2) { - this._setPictureUriOfSettingsObject(screensaver_setting, background_setting.get_string('picture-uri')); - } - - // Run general post command - let commandString = this._settings.get('general-post-command', 'string'); - let generalPostCommandArray = this._getCommandArray(commandString, path); - if (generalPostCommandArray !== null) { - try { - await Utils.Utils.execCheck(generalPostCommandArray); - } catch (error) { - this.logger.warn(error); - } - } - - // call callback if given - if (callback) { - callback(); - } - } - - /** - * Set the picture-uri property of the given settings object to the path. - * Precondition: the settings object has to be a valid Gio settings object with the picture-uri property. - * @param settings - * @param path - * @param callback - * @private - */ - _setPictureUriOfSettingsObject(settings, path, callback) { - /* - inspired from: - https://bitbucket.org/LukasKnuth/backslide/src/7e36a49fc5e1439fa9ed21e39b09b61eca8df41a/backslide@codeisland.org/settings.js?at=master - */ - let set_prop = (property) => { - if (settings.is_writable(property)) { - // Set a new Background-Image (should show up immediately): - if (!settings.set_string(property, path)) { - this._bailOutWithCallback(`Failed to write property: ${property}`, callback); - } - } else { - this._bailOutWithCallback(`Property not writable: ${property}`, callback); - } - } - - const availableKeys = settings.list_keys(); - - let property = "picture-uri"; - if (availableKeys.indexOf(property) !== -1) { - set_prop(property); - } - - property = "picture-uri-dark"; - if (availableKeys.indexOf(property) !== -1) { - set_prop(property); - } - - Gio.Settings.sync(); // Necessary: http://stackoverflow.com/questions/9985140 - - // call callback if given - if (callback) { - callback(); - } - } - - setWallpaper(historyId) { - let historyElement = this._historyController.get(historyId); - - if (this._historyController.promoteToActive(historyElement.id)) { - this._setBackground(historyElement.path); - } else { - this.logger.warn("The history id (" + historyElement.id + ") could not be found.") - // TODO: Error handling history id not found. - } - } - - fetchNewWallpaper(callback) { - this._startLoadingHooks.forEach((element) => { - element(); - }); - - if (!this._prohibitTimer) { - this._timer.reset(); // reset timer - } - - let returnObject = this._getRandomAdapter(); - returnObject.adapter.requestRandomImage((historyElement, error) => { - if (historyElement == null || error) { - this._bailOutWithCallback("Could not fetch wallpaper location.", callback); - this._stopLoadingHooks.map(element => element(null)); - return; - } - - this.logger.info("Requesting image: " + historyElement.source.imageDownloadUrl); - - returnObject.adapter.fetchFile(historyElement.source.imageDownloadUrl, (historyId, path, error) => { - if (error) { - this._bailOutWithCallback(`Could not load new wallpaper: ${error}`, callback); - this._stopLoadingHooks.forEach(element => element(null)); - return; - } - - historyElement.name = String(historyId); - historyElement.id = `${historyElement.timestamp}_${historyElement.name}`; // timestamp ensures uniqueness - historyElement.adapter.id = returnObject.adapterId; - historyElement.adapter.type = returnObject.adapterType; - - // Move file to unique naming - let sourceFile = Gio.File.new_for_path(path); - let targetFolder = sourceFile.get_parent(); - let targetFile = targetFolder.get_child(historyElement.id); - - try { - if (!sourceFile.move(targetFile, Gio.FileCopyFlags.NONE, null, null)) { - this.logger.warn('Failed copying unique image.'); - return; - } - } catch (error) { - if (error === Gio.IOErrorEnum.EXISTS) { - this.logger.warn('Image already exists in location.'); - return; - } - } - - historyElement.path = targetFile.get_path(); - - this._setBackground(historyElement.path, () => { - // insert file into history - this._historyController.insert(historyElement); - - this._stopLoadingHooks.forEach(element => element(null)); - - // call callback if given - if (callback) { - callback(); - } - }); - }); - }); - } - - // TODO: Change to original historyElement if more variable get exposed - _getCommandArray(commandString, historyElementPath) { - let string = commandString; - if (string === "") { - return null; - } - - // Replace variables - let variables = new Map(); - variables.set('%wallpaper_path%', historyElementPath); - - variables.forEach((value, key) => { - string = string.replaceAll(key, value); - }); - - try { - // https://gjs-docs.gnome.org/glib20/glib.shell_parse_argv - // Parses a command line into an argument vector, in much the same way - // the shell would, but without many of the expansions the shell would - // perform (variable expansion, globs, operators, filename expansion, - // etc. are not supported). - return GLib.shell_parse_argv(string)[1]; - } catch (e) { - this.logger.warn(e); - } - - return null; - } - - _backgroundTimeout(delay) { - if (this.timeout) { - return; - } - - delay = delay || 200; - - this.timeout = Mainloop.timeout_add(Mainloop.PRIORITY_DEFAULT, delay, () => { - this.timeout = null; - if (this._resetWallpaper) { - this._setBackground(this._historyController.getCurrentElement().path); - this._resetWallpaper = false; - } else { - this._setBackground(this.wallpaperLocation + this.previewId); - } - return false; - }); - } - - previewWallpaper(historyid, delay) { - if (!this._settings.get('disable-hover-preview', 'boolean')) { - this.previewId = historyid; - this._resetWallpaper = false; - - this._backgroundTimeout(delay); - } - } - - resetWallpaper() { - if (!this._settings.get('disable-hover-preview', 'boolean')) { - this._resetWallpaper = true; - this._backgroundTimeout(); - } - } - - getHistoryController() { - return this._historyController; - } - - deleteHistory() { - this._historyController.clear(); - } - - update() { - this._updateHistory(); - } - - registerStartLoadingHook(fn) { - if (typeof fn === "function") { - this._startLoadingHooks.push(fn) - } - } - - registerStopLoadingHook(fn) { - if (typeof fn === "function") { - this._stopLoadingHooks.push(fn) - } - } - - _bailOutWithCallback(msg, callback) { - this.logger.error(msg); - - if (callback) { - callback(); - } - } - -}; diff --git a/src/adapter/baseAdapter.ts b/src/adapter/baseAdapter.ts new file mode 100755 index 00000000..ae5d3930 --- /dev/null +++ b/src/adapter/baseAdapter.ts @@ -0,0 +1,124 @@ +import Gio from 'gi://Gio'; + +import * as SettingsModule from './../settings.js'; + +import {HistoryEntry} from './../history.js'; +import {Logger} from './../logger.js'; +import {SoupBowl} from './../soupBowl.js'; + +/** + * Abstract base adapter for subsequent classes to implement. + */ +abstract class BaseAdapter { + protected _bowl = new SoupBowl(); + + protected _generalSettings: SettingsModule.Settings; + protected _logger: Logger; + protected _settings: SettingsModule.Settings; + protected _sourceName: string; + + /** + * Create a new base adapter. + * + * Exposes settings and utilities for subsequent classes. + * Previously saved settings will be used if the ID matches. + * + * @param {object} params Parameter object with settings + * @param {string} params.defaultName Default adapter name + * @param {string} params.id Unique ID + * @param {string | null} params.name Custom name, falls back to the default name on null + * @param {string} params.schemaID ID of the adapter specific schema ID + * @param {string} params.schemaPath Path to the adapter specific settings schema + */ + constructor(params: { + defaultName: string; + id: string; + name: string | null; + schemaID: string; + schemaPath: string; + }) { + const path = `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${params.id}/`; + this._logger = new Logger('RWG3', `${params.defaultName} adapter`); + + this._settings = new SettingsModule.Settings(params.schemaID, params.schemaPath); + this._sourceName = params.name ?? params.defaultName; + + this._generalSettings = new SettingsModule.Settings( + SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, + path + ); + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + abstract requestRandomImage (count: number): Promise; + + /** + * Fetches an image according to a given HistoryEntry. + * + * This default implementation requests the image in HistoryEntry.source.imageDownloadUrl + * using Soup and saves it to HistoryEntry.path + * + * @param {HistoryEntry} historyEntry The historyEntry to fetch + * @returns {Promise} unaltered HistoryEntry + */ + async fetchFile(historyEntry: HistoryEntry): Promise { + const file = Gio.file_new_for_path(historyEntry.path); + const fstream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); + + // craft new message from details + const request = this._bowl.newGetMessage(historyEntry.source.imageDownloadUrl); + + // start the download + const response_data_bytes = await this._bowl.send_and_receive(request); + if (!response_data_bytes) { + fstream.close(null); + throw new Error('Not a valid image response'); + } + + fstream.write(response_data_bytes, null); + fstream.close(null); + + return historyEntry; + } + + /** + * Check if an array already contains a matching HistoryEntry. + * + * @param {HistoryEntry[]} array Array to search in + * @param {string} uri URI to search for + * @returns {boolean} Whether the array contains an item with $uri + */ + protected _includesWallpaper(array: HistoryEntry[], uri: string): boolean { + for (const element of array) { + if (element.source.imageDownloadUrl === uri) + return true; + } + + return false; + } + + /** + * Check if this image is in the list of blocked images. + * + * @param {string} filename Name of the image + * @returns {boolean} Whether the image is blocked + */ + protected _isImageBlocked(filename: string): boolean { + const blockedFilenames = this._generalSettings.getStrv('blocked-images'); + + if (blockedFilenames.includes(filename)) { + this._logger.info(`Image is blocked: ${filename}`); + return true; + } + + return false; + } +} + +export {BaseAdapter}; diff --git a/src/adapter/genericJson.ts b/src/adapter/genericJson.ts new file mode 100644 index 00000000..9bb63272 --- /dev/null +++ b/src/adapter/genericJson.ts @@ -0,0 +1,179 @@ +import * as JSONPath from './../jsonPath.js'; +import * as SettingsModule from './../settings.js'; +import * as Utils from './../utils.js'; + +import {BaseAdapter} from './../adapter/baseAdapter.js'; +import {HistoryEntry} from './../history.js'; + +/** How many times the service should be queried at maximum. */ +const MAX_SERVICE_RETRIES = 5; +/** + * How many times we should try to get a new image from an array. + * No new request are being made. + */ +const MAX_ARRAY_RETRIES = 5; + +/** + * Adapter for generic JSON image sources. + */ +class GenericJsonAdapter extends BaseAdapter { + /** + * Create a new generic json adapter. + * + * @param {string} id Unique ID + * @param {string} name Custom name of this adapter + */ + constructor(id: string, name: string) { + super({ + defaultName: 'Generic JSON Source', + id, + name, + schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON, + schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/genericJSON/${id}/`, + }); + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + private async _getHistoryEntry(count: number): Promise { + const wallpaperResult: HistoryEntry[] = []; + + let url = this._settings.getString('request-url'); + url = encodeURI(url); + + const message = this._bowl.newGetMessage(url); + if (message === null) { + this._logger.error('Could not create request.'); + throw wallpaperResult; + } + + let response_body; + try { + const response_body_bytes = await this._bowl.send_and_receive(message); + response_body = JSON.parse(new TextDecoder().decode(response_body_bytes)) as unknown; + } catch (error) { + this._logger.error(error); + throw wallpaperResult; + } + + const imageJSONPath = this._settings.getString('image-path'); + const postJSONPath = this._settings.getString('post-path'); + const domainUrl = this._settings.getString('domain'); + const authorNameJSONPath = this._settings.getString('author-name-path'); + const authorUrlJSONPath = this._settings.getString('author-url-path'); + + for (let i = 0; i < MAX_ARRAY_RETRIES + count && wallpaperResult.length < count; i++) { + const [returnObject, resolvedPath] = JSONPath.getTarget(response_body, imageJSONPath); + if (!returnObject || (typeof returnObject !== 'string' && typeof returnObject !== 'number') || returnObject === '') { + this._logger.error('Unexpected json member found'); + break; + } + + const imageDownloadUrl = this._settings.getString('image-prefix') + String(returnObject); + const imageBlocked = this._isImageBlocked(Utils.fileName(imageDownloadUrl)); + + // Don't retry without @random present in JSONPath + if (imageBlocked && !imageJSONPath.includes('@random')) { + // Abort and try again + break; + } + + if (imageBlocked) + continue; + + // A bit cumbersome to handle "unknown" in the following parts: + // https://github.com/microsoft/TypeScript/issues/27706 + + let postUrl: string; + const postUrlObject = JSONPath.getTarget(response_body, JSONPath.replaceRandomInPath(postJSONPath, resolvedPath))[0]; + if (typeof postUrlObject === 'string' || typeof postUrlObject === 'number') + postUrl = this._settings.getString('post-prefix') + String(postUrlObject); + else + postUrl = ''; + + let authorName: string | null = null; + const authorNameObject = JSONPath.getTarget(response_body, JSONPath.replaceRandomInPath(authorNameJSONPath, resolvedPath))[0]; + if (typeof authorNameObject === 'string' && authorNameObject !== '') + authorName = authorNameObject; + + let authorUrl: string; + const authorUrlObject = JSONPath.getTarget(response_body, JSONPath.replaceRandomInPath(authorUrlJSONPath, resolvedPath))[0]; + if (typeof authorUrlObject === 'string' || typeof authorUrlObject === 'number') + authorUrl = this._settings.getString('author-url-prefix') + String(authorUrlObject); + else + authorUrl = ''; + + const historyEntry = new HistoryEntry(authorName, this._sourceName, imageDownloadUrl); + + if (authorUrl !== '') + historyEntry.source.authorUrl = authorUrl; + + if (postUrl !== '') + historyEntry.source.imageLinkUrl = postUrl; + + if (domainUrl !== '') + historyEntry.source.sourceUrl = domainUrl; + + if (!this._includesWallpaper(wallpaperResult, historyEntry.source.imageDownloadUrl)) + wallpaperResult.push(historyEntry); + } + + if (wallpaperResult.length < count) { + this._logger.warn('Returning less images than requested.'); + throw wallpaperResult; + } + + return wallpaperResult; + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * Can internally query the request URL multiple times because it's unknown how many images will be reported back. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + async requestRandomImage(count: number): Promise { + const wallpaperResult: HistoryEntry[] = []; + + for (let i = 0; i < MAX_SERVICE_RETRIES + count && wallpaperResult.length < count; i++) { + let historyArray: HistoryEntry[] = []; + + try { + // This should run sequentially + // eslint-disable-next-line no-await-in-loop + historyArray = await this._getHistoryEntry(count); + } catch (error) { + this._logger.warn('Failed getting image'); + + if (Array.isArray(error) && error.length > 0 && error[0] instanceof HistoryEntry) + historyArray = error as HistoryEntry[]; + + // Do not escalate yet, try again + } finally { + historyArray.forEach(element => { + if (!this._includesWallpaper(wallpaperResult, element.source.imageDownloadUrl)) + wallpaperResult.push(element); + }); + } + + // Image blocked, try again + } + + if (wallpaperResult.length < count) { + this._logger.warn('Returning less images than requested.'); + throw wallpaperResult; + } + + return wallpaperResult; + } +} + +export {GenericJsonAdapter}; diff --git a/src/adapter/localFolder.ts b/src/adapter/localFolder.ts new file mode 100644 index 00000000..0426848f --- /dev/null +++ b/src/adapter/localFolder.ts @@ -0,0 +1,135 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +import * as SettingsModule from './../settings.js'; +import * as Utils from './../utils.js'; + +import {BaseAdapter} from './../adapter/baseAdapter.js'; +import {HistoryEntry} from './../history.js'; + +// https://gjs.guide/guides/gjs/asynchronous-programming.html#promisify-helper +Gio._promisify(Gio.File.prototype, 'copy_async', 'copy_finish'); + +/** + * Adapter for fetching from the local filesystem. + */ +class LocalFolderAdapter extends BaseAdapter { + /** + * Create a new local folder adapter. + * + * @param {string} id Unique ID + * @param {string} name Custom name of this adapter + */ + constructor(id: string, name: string) { + super({ + defaultName: 'Local Folder', + id, + name, + schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER, + schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/localFolder/${id}/`, + }); + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + requestRandomImage(count: number): Promise { + return new Promise((resolve, reject) => { + const folder = Gio.File.new_for_path(this._settings.getString('folder')); + const files = this._listDirectory(folder); + const wallpaperResult: HistoryEntry[] = []; + + if (files.length < 1) { + this._logger.error('No files found'); + reject(wallpaperResult); + return; + } + this._logger.debug(`Found ${files.length} possible wallpaper in "${this._settings.getString('folder')}"`); + + const shuffledFiles = Utils.shuffleArray(files); + + for (let i = 0; i < count && i < shuffledFiles.length; i++) { + const randomFile = shuffledFiles[i]; + const randomFilePath = randomFile.get_uri(); + + const historyEntry = new HistoryEntry(null, this._sourceName, randomFilePath); + historyEntry.source.sourceUrl = randomFilePath; + + wallpaperResult.push(historyEntry); + } + + if (wallpaperResult.length < count) { + this._logger.warn('Returning less images than requested.'); + reject(wallpaperResult); + return; + } + + resolve(wallpaperResult); + }); + } + + /** + * Copies a file from the filesystem to the destination folder. + * + * @param {HistoryEntry} historyEntry The historyEntry to fetch + * @returns {Promise} unaltered HistoryEntry + */ + async fetchFile(historyEntry: HistoryEntry): Promise { + const sourceFile = Gio.File.new_for_uri(historyEntry.source.imageDownloadUrl); + const targetFile = Gio.File.new_for_path(historyEntry.path); + + // https://gjs.guide/guides/gio/file-operations.html#copying-and-moving-files + // @ts-expect-error This function was rewritten by Gio._promisify + // eslint-disable-next-line @typescript-eslint/await-thenable + if (!await sourceFile.copy_async(targetFile, Gio.FileCopyFlags.NONE, GLib.PRIORITY_DEFAULT, null, null)) + throw new Error('Failed copying image.'); + + return historyEntry; + } + + // https://gjs.guide/guides/gio/file-operations.html#recursively-deleting-a-directory + /** + * Walk recursively through a folder and retrieve a list of all images. + * + * This already checks for blocked filenames and omits them from the returned list. + * + * @param {Gio.File} directory Directory to scan + * @returns {Gio.File[]} List of images + */ + private _listDirectory(directory: Gio.File): Gio.File[] { + const iterator = directory.enumerate_children('standard::*', Gio.FileQueryInfoFlags.NONE, null); + + let files: Gio.File[] = []; + 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; + } + + const contentType = info.get_content_type(); + const filename = child.get_basename(); + if (contentType?.startsWith('image/') && filename && !this._isImageBlocked(filename)) + files.push(child); + } + + return files; + } +} + +export {LocalFolderAdapter}; diff --git a/src/adapter/reddit.ts b/src/adapter/reddit.ts new file mode 100644 index 00000000..9aae7ab6 --- /dev/null +++ b/src/adapter/reddit.ts @@ -0,0 +1,162 @@ +import * as SettingsModule from './../settings.js'; +import * as Utils from './../utils.js'; + +import {BaseAdapter} from './../adapter/baseAdapter.js'; +import {HistoryEntry} from './../history.js'; + +interface RedditResponse { + data: { + children: RedditSubmission[], + } +} + +interface RedditSubmission { + data: { + post_hint: string, + over_18: boolean, + subreddit_name_prefixed: string, + permalink: string, + preview: { + images: { + source: { + width: number, + height: number, + url: string, + } + }[] + } + } +} + +/** + * Adapter for Reddit image sources. + */ +class RedditAdapter extends BaseAdapter { + /** + * Create a new Reddit adapter. + * + * @param {string} id Unique ID + * @param {string} name Custom name of this adapter + */ + constructor(id: string, name: string) { + super({ + defaultName: 'Reddit', + id, + name, + schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_REDDIT, + schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/reddit/${id}/`, + }); + } + + /** + * Replace an HTML & with an actual & symbol. + * + * @param {string} string String to replace in + * @returns {string} String with replaced symbols + */ + private _ampDecode(string: string): string { + return string.replace(/&/g, '&'); + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + async requestRandomImage(count: number): Promise { + const wallpaperResult: HistoryEntry[] = []; + const subreddits = this._settings.getString('subreddits').split(',').map(s => s.trim()).join('+'); + const require_sfw = this._settings.getBoolean('allow-sfw'); + + const url = encodeURI(`https://www.reddit.com/r/${subreddits}.json`); + const message = this._bowl.newGetMessage(url); + + let response_body; + try { + const response_body_bytes = await this._bowl.send_and_receive(message); + response_body = JSON.parse(new TextDecoder().decode(response_body_bytes)) as unknown; + } catch (error) { + this._logger.error(error); + throw wallpaperResult; + } + + if (!this._isRedditResponse(response_body)) { + this._logger.error('Unexpected response'); + throw wallpaperResult; + } + + const filteredSubmissions = response_body.data.children.filter(child => { + if (child.data.post_hint !== 'image') + return false; + if (require_sfw) + return child.data.over_18 === false; + + const minWidth = this._settings.getInt('min-width'); + const minHeight = this._settings.getInt('min-height'); + if (child.data.preview.images[0].source.width < minWidth) + return false; + if (child.data.preview.images[0].source.height < minHeight) + return false; + + const imageRatio1 = this._settings.getInt('image-ratio1'); + const imageRatio2 = this._settings.getInt('image-ratio2'); + if (child.data.preview.images[0].source.width / imageRatio1 * imageRatio2 < child.data.preview.images[0].source.height) + return false; + return true; + }); + + if (filteredSubmissions.length === 0) { + this._logger.error('No suitable submissions found!'); + throw wallpaperResult; + } + + for (let i = 0; i < filteredSubmissions.length && wallpaperResult.length < count; i++) { + const random = Utils.getRandomNumber(filteredSubmissions.length); + const submission = filteredSubmissions[random].data; + const imageDownloadUrl = this._ampDecode(submission.preview.images[0].source.url); + + if (this._isImageBlocked(Utils.fileName(imageDownloadUrl))) + continue; + + const historyEntry = new HistoryEntry(null, this._sourceName, imageDownloadUrl); + historyEntry.source.sourceUrl = `https://www.reddit.com/${submission.subreddit_name_prefixed}`; + historyEntry.source.imageLinkUrl = `https://www.reddit.com/${submission.permalink}`; + + if (!this._includesWallpaper(wallpaperResult, historyEntry.source.imageDownloadUrl)) + wallpaperResult.push(historyEntry); + } + + if (wallpaperResult.length < count) { + this._logger.warn('Returning less images than requested.'); + throw wallpaperResult; + } + + return wallpaperResult; + } + + /** + * Check if the response is expected to be a response by Reddit. + * + * Primarily in use for typescript typing. + * + * @param {unknown} object Unknown object to narrow down + * @returns {boolean} Whether the response is from Reddit + */ + private _isRedditResponse(object: unknown): object is RedditResponse { + if (typeof object === 'object' && + object && + 'data' in object && + typeof object.data === 'object' && + object.data && + 'children' in object.data && + Array.isArray(object.data.children) + ) + return true; + + return false; + } +} + +export {RedditAdapter}; diff --git a/src/adapter/unsplash.ts b/src/adapter/unsplash.ts new file mode 100644 index 00000000..d6e99bd4 --- /dev/null +++ b/src/adapter/unsplash.ts @@ -0,0 +1,242 @@ +import * as SettingsModule from './../settings.js'; +import * as Utils from './../utils.js'; + +import {BaseAdapter} from './../adapter/baseAdapter.js'; +import {HistoryEntry} from './../history.js'; + +/** How many times the service should be queried at maximum. */ +const MAX_SERVICE_RETRIES = 5; + +// Generated code produces a no-shadow rule error +/* eslint-disable */ +enum ConstraintType { + UNCONSTRAINED, + USER, + USERS_LIKES, + COLLECTION_ID, +} +/* eslint-enable */ + +/** + * Adapter for image sources using Unsplash. + */ +class UnsplashAdapter extends BaseAdapter { + private _sourceUrl = 'https://source.unsplash.com'; + + // default query options + private _options = { + 'query': '', + 'w': 1920, + 'h': 1080, + 'featured': false, + 'constraintType': 0, + 'constraintValue': '', + }; + + /** + * Create a new Unsplash adapter. + * + * @param {string} id Unique ID + * @param {string} name Custom name of this adapter + */ + constructor(id: string | null, name: string | null) { + super({ + defaultName: 'Unsplash', + id: id ?? '-1', + name, + schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH, + schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/unsplash/${id ?? '-1'}/`, + }); + } + + /** + * Retrieves a new URL for an image and crafts new HistoryEntry. + * + * @returns {HistoryEntry} Crafted HistoryEntry + * @throws {Error} Error with description + */ + private async _getHistoryEntry(): Promise { + this._readOptionsFromSettings(); + const optionsString = this._generateOptionsString(); + + let url = `https://source.unsplash.com${optionsString}`; + url = encodeURI(url); + + this._logger.debug(`Unsplash request to: ${url}`); + + const message = this._bowl.newGetMessage(url); + + // unsplash redirects to actual file; we only want the file location + message.set_flags(this._bowl.MessageFlags.NO_REDIRECT); + + await this._bowl.send_and_receive(message); + + // expecting redirect + if (message.status_code !== 302) + throw new Error('Unexpected response status code (expected 302)'); + + const imageLinkUrl = message.response_headers.get_one('Location'); + if (!imageLinkUrl) + throw new Error('No image link in response.'); + + + if (this._isImageBlocked(Utils.fileName(imageLinkUrl))) { + // Abort and try again + throw new Error('Image blocked'); + } + + const historyEntry = new HistoryEntry(null, this._sourceName, imageLinkUrl); + historyEntry.source.sourceUrl = this._sourceUrl; + historyEntry.source.imageLinkUrl = imageLinkUrl; + + return historyEntry; + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * Can internally query the request URL multiple times because only one image will be reported back. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + async requestRandomImage(count: number): Promise { + const wallpaperResult: HistoryEntry[] = []; + + for (let i = 0; i < MAX_SERVICE_RETRIES + count && wallpaperResult.length < count; i++) { + try { + // This should run sequentially + // eslint-disable-next-line no-await-in-loop + const historyEntry = await this._getHistoryEntry(); + + if (!this._includesWallpaper(wallpaperResult, historyEntry.source.imageDownloadUrl)) + wallpaperResult.push(historyEntry); + } catch (error) { + this._logger.warn('Failed getting image.'); + this._logger.warn(error); + // Do not escalate yet, try again + } + + // Image blocked, try again + } + + if (wallpaperResult.length < count) { + this._logger.warn('Returning less images than requested.'); + throw wallpaperResult; + } + + return wallpaperResult; + } + + /** + * Create an option string based on user settings. + * + * Does not refresh settings itself. + * + * @returns {string} Options string + */ + private _generateOptionsString(): string { + const options = this._options; + let optionsString = ''; + + switch (options.constraintType) { + case 1: + optionsString = `/user/${options.constraintValue}/`; + break; + case 2: + optionsString = `/user/${options.constraintValue}/likes/`; + break; + case 3: + 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) { + const q = options.query.replace(/\W/, ','); + optionsString += `?${q}`; + } + + return optionsString; + } + + /** + * Freshly read the user settings options. + */ + private _readOptionsFromSettings(): void { + this._options.w = this._settings.getInt('image-width'); + this._options.h = this._settings.getInt('image-height'); + + this._options.constraintType = this._settings.getInt('constraint-type'); + this._options.constraintValue = this._settings.getString('constraint-value'); + + const keywords = this._settings.getString('keyword').split(','); + if (keywords.length > 0) { + const randomKeyword = keywords[Utils.getRandomNumber(keywords.length)]; + this._options.query = randomKeyword.trim(); + } + + this._options.featured = this._settings.getBoolean('featured-only'); + } +} + +/** + * Retrieve the human readable enum name. + * + * @param {ConstraintType} type The type to name + * @returns {string} Name + */ +function _getConstraintTypeName(type: ConstraintType): string { + let name: string; + + switch (type) { + case ConstraintType.UNCONSTRAINED: + name = 'Unconstrained'; + break; + case ConstraintType.USER: + name = 'User'; + break; + case ConstraintType.USERS_LIKES: + name = 'User\'s Likes'; + break; + case ConstraintType.COLLECTION_ID: + name = 'Collection ID'; + break; + + default: + name = 'Constraint type name not found'; + break; + } + + return name; +} + +/** + * Get a list of human readable enum entries. + * + * @returns {string[]} Array with key names + */ +function getConstraintTypeNameList(): string[] { + const list: string[] = []; + + const values = Object.values(ConstraintType).filter(v => !isNaN(Number(v))); + for (const i of values) + list.push(_getConstraintTypeName(i as ConstraintType)); + + return list; +} + +export { + UnsplashAdapter, + ConstraintType, + getConstraintTypeNameList +}; diff --git a/src/adapter/urlSource.ts b/src/adapter/urlSource.ts new file mode 100644 index 00000000..23060107 --- /dev/null +++ b/src/adapter/urlSource.ts @@ -0,0 +1,79 @@ +import * as SettingsModule from './../settings.js'; + +import {BaseAdapter} from './../adapter/baseAdapter.js'; +import {HistoryEntry} from './../history.js'; + +/** + * Adapter for using a single static URL as an image source. + */ +class UrlSourceAdapter extends BaseAdapter { + /** + * Create a new static url adapter. + * + * @param {string} id Unique ID + * @param {string} name Custom name of this adapter + */ + constructor(id: string, name: string) { + super({ + defaultName: 'Static URL', + id, + name, + schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE, + schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/urlSource/${id}/`, + }); + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * Can internally query the request URL multiple times because only one image will be reported back. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + requestRandomImage(count: number): Promise { + const wallpaperResult: HistoryEntry[] = []; + + let requestedEntries = 1; + if (this._settings.getBoolean('different-images')) + requestedEntries = count; + + const imageDownloadUrl = this._settings.getString('image-url'); + let authorName: string | null = this._settings.getString('author-name'); + const authorUrl = this._settings.getString('author-url'); + const domainUrl = this._settings.getString('domain'); + const postUrl = this._settings.getString('domain'); + + if (imageDownloadUrl === '') { + this._logger.error('Missing download url'); + throw wallpaperResult; + } + + if (authorName === '') + authorName = null; + + for (let i = 0; i < requestedEntries; i++) { + const historyEntry = new HistoryEntry(authorName, this._sourceName, imageDownloadUrl); + + if (authorUrl !== '') + historyEntry.source.authorUrl = authorUrl; + + if (postUrl !== '') + historyEntry.source.imageLinkUrl = postUrl; + + if (domainUrl !== '') + historyEntry.source.sourceUrl = domainUrl; + + // overwrite historyEntry.id because the name will be the same and the timestamp might be too. + // historyEntry.name can't be null here because we created the entry with the constructor. + historyEntry.id = `${historyEntry.timestamp}_${i}_${historyEntry.name!}`; + + wallpaperResult.push(historyEntry); + } + + return Promise.resolve(wallpaperResult); + } +} + +export {UrlSourceAdapter}; diff --git a/src/adapter/wallhaven.ts b/src/adapter/wallhaven.ts new file mode 100644 index 00000000..fcd9ab50 --- /dev/null +++ b/src/adapter/wallhaven.ts @@ -0,0 +1,240 @@ +import Gio from 'gi://Gio'; + +import * as SettingsModule from './../settings.js'; +import * as Utils from './../utils.js'; + +import {BaseAdapter} from './../adapter/baseAdapter.js'; +import {HistoryEntry} from './../history.js'; + +interface QueryOptions { + /** + * Filter AI generated images. + * + * - 0 = Include them in search results + * - 1 = Don't include them in search results + */ + ai_art_filter: string, + + atleast: string, + categories: string, + colors: string, + purity: string, + q: string, + ratios: string[], + sorting: string, +} + +interface WallhavenSearchResponse { + data: { + path: string, + url: string, + }[] +} + +/** + * Adapter for Wallhaven image sources. + */ +class WallhavenAdapter extends BaseAdapter { + private _options: QueryOptions = { + ai_art_filter: '1', + q: '', + purity: '110', // SFW, sketchy + sorting: 'random', + categories: '111', // General, Anime, People + atleast: '1920x1080', + ratios: ['16x9'], + colors: '', + }; + + /** + * Create a new wallhaven adapter. + * + * @param {string} id Unique ID + * @param {string} name Custom name of this adapter + */ + constructor(id: string, name: string) { + super({ + id, + schemaID: SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN, + schemaPath: `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/wallhaven/${id}/`, + name, + defaultName: 'Wallhaven', + }); + } + + /** + * Retrieves new URLs for images and crafts new HistoryEntries. + * + * @param {number} count Number of requested wallpaper + * @returns {HistoryEntry[]} Array of crafted HistoryEntries + * @throws {HistoryEntry[]} Array of crafted historyEntries, can be empty + */ + async requestRandomImage(count: number): Promise { + const wallpaperResult: HistoryEntry[] = []; + + this._readOptionsFromSettings(); + const optionsString = this._generateOptionsString(this._options); + + const url = `https://wallhaven.cc/api/v1/search?${encodeURI(optionsString)}`; + const message = this._bowl.newGetMessage(url); + + const apiKey = this._settings.getString('api-key'); + if (apiKey !== '') + message.requestHeaders.append('X-API-Key', apiKey); + + this._logger.debug(`Search URL: ${url}`); + + let wallhavenResponse; + try { + const response_body_bytes = await this._bowl.send_and_receive(message); + wallhavenResponse = JSON.parse(new TextDecoder().decode(response_body_bytes)) as unknown; + } catch (error) { + this._logger.error(error); + throw wallpaperResult; + } + + if (!this._isWallhavenResponse(wallhavenResponse)) { + this._logger.error('Unexpected response'); + throw wallpaperResult; + } + + const response = wallhavenResponse.data; + if (!response || response.length === 0) { + this._logger.error('Empty response'); + throw wallpaperResult; + } + + for (let i = 0; i < response.length && wallpaperResult.length < count; i++) { + const entry = response[i]; + const siteURL = entry.url; + const downloadURL = entry.path; + + if (this._isImageBlocked(Utils.fileName(downloadURL))) + continue; + + const historyEntry = new HistoryEntry(null, this._sourceName, downloadURL); + historyEntry.source.sourceUrl = 'https://wallhaven.cc/'; + historyEntry.source.imageLinkUrl = siteURL; + + if (!this._includesWallpaper(wallpaperResult, historyEntry.source.imageDownloadUrl)) + wallpaperResult.push(historyEntry); + } + + if (wallpaperResult.length < count) { + this._logger.warn('Returning less images than requested.'); + throw wallpaperResult; + } + + return wallpaperResult; + } + + /** + * Fetches an image according to a given HistoryEntry. + * + * This implementation requests the image in HistoryEntry.source.imageDownloadUrl + * using Soup and saves it to HistoryEntry.path while setting the X-API-Key header. + * + * @param {HistoryEntry} historyEntry The historyEntry to fetch + * @returns {Promise} unaltered HistoryEntry + */ + async fetchFile(historyEntry: HistoryEntry): Promise { + const file = Gio.file_new_for_path(historyEntry.path); + const fstream = file.replace(null, false, Gio.FileCreateFlags.NONE, null); + + // craft new message from details + const request = this._bowl.newGetMessage(historyEntry.source.imageDownloadUrl); + + const apiKey = this._settings.getString('api-key'); + if (apiKey !== '') + request.requestHeaders.append('X-API-Key', apiKey); + + // start the download + const response_data_bytes = await this._bowl.send_and_receive(request); + if (!response_data_bytes) { + fstream.close(null); + throw new Error('Not a valid image response'); + } + + fstream.write(response_data_bytes, null); + fstream.close(null); + + return historyEntry; + } + + /** + * Create an option string based on user settings. + * + * Does not refresh settings itself. + * + * @param {QueryOptions} options Options to check + * @returns {string} Options string + */ + private _generateOptionsString(options: T): string { + let optionsString = ''; + + for (const key in options) { + if (options.hasOwnProperty(key)) { + if (Array.isArray(options[key])) + optionsString += `${key}=${(options[key] as Array).join()}&`; + else if (typeof options[key] === 'string' && options[key] !== '') + optionsString += `${key}=${options[key] as string}&`; + } + } + + return optionsString; + } + + /** + * Check if the response is expected to be a response by Wallhaven. + * + * Primarily in use for typescript typing. + * + * @param {unknown} object Unknown object to narrow down + * @returns {boolean} Whether the response is from Reddit + */ + private _isWallhavenResponse(object: unknown): object is WallhavenSearchResponse { + if (typeof object === 'object' && + object && + 'data' in object && + Array.isArray(object.data) + ) + return true; + + return false; + } + + /** + * Freshly read the user settings options. + */ + private _readOptionsFromSettings(): void { + const keywords = this._settings.getString('keyword').split(','); + if (keywords.length > 0) { + const randomKeyword = keywords[Utils.getRandomNumber(keywords.length)]; + this._options.q = randomKeyword.trim(); + } + + this._options.atleast = this._settings.getString('minimal-resolution'); + this._options.ratios = this._settings.getString('aspect-ratios').split(','); + this._options.ratios = this._options.ratios.map(elem => { + return elem.trim(); + }); + + const categories = []; + categories.push(Number(this._settings.getBoolean('category-general'))); + categories.push(Number(this._settings.getBoolean('category-anime'))); + categories.push(Number(this._settings.getBoolean('category-people'))); + this._options.categories = categories.join(''); + + const purity = []; + purity.push(Number(this._settings.getBoolean('allow-sfw'))); + purity.push(Number(this._settings.getBoolean('allow-sketchy'))); + purity.push(Number(this._settings.getBoolean('allow-nsfw'))); + this._options.purity = purity.join(''); + + this._options.ai_art_filter = this._settings.getBoolean('ai-art') ? '0' : '1'; + + this._options.colors = this._settings.getString('color'); + } +} + +export {WallhavenAdapter}; diff --git a/src/extension.ts b/src/extension.ts new file mode 100644 index 00000000..f6ae12ee --- /dev/null +++ b/src/extension.ts @@ -0,0 +1,150 @@ +// Use legacy style importing to work around standard imports not available in files loaded by the shell, those can't be modules (yet) +// > Note that as of GNOME 44, neither GNOME Shell nor Extensions support ESModules, and must use GJS custom import scheme. +// https://gjs.guide/extensions/overview/imports-and-modules.html#imports-and-modules +// https://gjs-docs.gnome.org/gjs/esmodules.md +// > JS ERROR: Extension randomwallpaper@iflow.space: SyntaxError: import declarations may only appear at top level of a module +// For correct typing use: 'InstanceType' +const GLib = imports.gi.GLib; + +import type * as LoggerNamespace from './logger.js'; +import type * as AFTimer from './timer.js'; +import type * as WallpaperControllerNamespace from './wallpaperController.js'; +import type * as RandomWallpaperMenuNamespace from './randomWallpaperMenu.js'; +import type {ExtensionMeta} from 'ExtensionMeta'; + +let Logger: typeof LoggerNamespace | null = null; +let Timer: typeof AFTimer | null = null; +let WallpaperController: typeof WallpaperControllerNamespace | null = null; +let RandomWallpaperMenu: typeof RandomWallpaperMenuNamespace | null = null; + +/** + * This function is called once when your extension is loaded, not enabled. This + * is a good time to setup translations or anything else you only do once. + * + * You MUST NOT make any changes to GNOME Shell, connect any signals or add any + * MainLoop sources here. + * + * @param {ExtensionMeta} unusedMeta An extension meta object, https://gjs.guide/extensions/overview/anatomy.html#extension-meta-object + * @returns {Extension} an object with enable() and disable() methods + */ +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +function init(unusedMeta: ExtensionMeta): Extension { + return new Extension(); +} + +/** + * Own extension class object. Entry point for Gnome Shell hooks. + * + * The functions enable() and disable() are required. + */ +class Extension { + private _logger: LoggerNamespace.Logger | null = null; + private _wallpaperController: WallpaperControllerNamespace.WallpaperController | null = null; + private _panelMenu: RandomWallpaperMenuNamespace.RandomWallpaperMenu | null = null; + private _timer: AFTimer.AFTimer | null = null; + + /** + * This function is called when your extension is enabled, which could be + * done in GNOME Extensions, when you log in or when the screen is unlocked. + * + * This is when you should setup any UI for your extension, change existing + * widgets, connect signals or modify GNOME Shell's behavior. + */ + enable(): void { + // Workaround crash when initializing the gnome shell with this extension active while being on X11 + // https://gitlab.gnome.org/GNOME/gnome-shell/-/issues/6691 + // TODO: Remove once that issue is fixed. + const crashWorkaround = new Promise(resolve => { + GLib.timeout_add(GLib.PRIORITY_HIGH, 100, () => { + resolve(); + return GLib.SOURCE_REMOVE; + }); + }); + + // Dynamically load own modules. This allows us to use proper ES6 Modules + crashWorkaround.then(() => { + this._importModules().then(() => { + if (!Logger || !Timer || !WallpaperController || !RandomWallpaperMenu) + throw new Error('Error importing module'); + + this._logger = new Logger.Logger('RWG3', 'Main'); + this._timer = Timer.AFTimer.getTimer(); + this._wallpaperController = new WallpaperController.WallpaperController(); + this._panelMenu = new RandomWallpaperMenu.RandomWallpaperMenu(this._wallpaperController); + + this._logger.info('Enable extension.'); + this._panelMenu.init(); + }).catch(error => { + if (this._logger) + this._logger.error(error); + else if (error instanceof Error) + logError(error); + else + logError(new Error('Unknown error')); + }); + }).catch(error => { + if (error instanceof Error) + logError(error); + else + logError(new Error('Unknown error')); + }); + } + + /** + * This function is called when your extension is uninstalled, disabled in + * GNOME Extensions, when you log out or when the screen locks. + * + * Anything you created, modified or setup in enable() MUST be undone here. + * Not doing so is the most common reason extensions are rejected in review! + */ + disable(): void { + if (this._logger) + this._logger.info('Disable extension.'); + + if (this._panelMenu) + this._panelMenu.cleanup(); + + // cleanup the timer singleton + if (Timer) + Timer.AFTimer.destroy(); + + if (this._wallpaperController) + this._wallpaperController.cleanup(); + + this._timer = null; + this._logger = null; + this._panelMenu = null; + this._wallpaperController = null; + + Timer = null; + Logger = null; + WallpaperController = null; + RandomWallpaperMenu = null; + } + + /** + * Import helper function. + * + * Loads all required modules async. + * This allows to omit the legacy GJS style imports (`const asd = imports.gi.asd`) + * and use proper modules for subsequent files. + * + * When the shell allows proper modules for loaded files (extension.js and prefs.js) + * this function can be removed and replaced by normal import statements. + */ + private async _importModules(): Promise { + const loggerPromise = import('./logger.js'); + const timerPromise = import('./timer.js'); + const wallpaperPromise = import('./wallpaperController.js'); + const menuPromise = import('./randomWallpaperMenu.js'); + + const [moduleLogger, moduleTimer, moduleWallpaper, moduleMenu] = await Promise.all([ + loggerPromise, timerPromise, wallpaperPromise, menuPromise, + ]); + + Logger = moduleLogger; + Timer = moduleTimer; + WallpaperController = moduleWallpaper; + RandomWallpaperMenu = moduleMenu; + } +} diff --git a/src/history.ts b/src/history.ts new file mode 100644 index 00000000..512fb005 --- /dev/null +++ b/src/history.ts @@ -0,0 +1,300 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +import * as Utils from './utils.js'; + +import {Logger} from './logger.js'; +import {Settings} from './settings.js'; + +// Gets filled by the HistoryController which is constructed at extension startup +let _wallpaperLocation: string; + +interface SourceInfo { + author: string | null; + authorUrl: string | null; + source: string | null; + sourceUrl: string | null; + imageDownloadUrl: string; + imageLinkUrl: string | null; +} + +interface AdapterInfo { + /** Identifier to access the settings path */ + id: string | null; + /** Adapter type as enum */ + type: number | null; +} + +/** + * Defines an image with core properties. + */ +class HistoryEntry { + timestamp = new Date().getTime(); + /** Unique identifier, concat of timestamp and name */ + id: string; + /** Basename of URI */ + name: string | null; // This can be null when an entry from an older version is mapped from settings + path: string; + source: SourceInfo; + adapter: AdapterInfo | null = { // This can be null when an entry from an older version is mapped from settings + id: null, + type: null, + }; + + /** + * Create a new HistoryEntry. + * + * The name, id, and path will be prefilled. + * + * @param {string | null} author Author of the image or null + * @param {string | null} source The image source or null + * @param {string} url The request URL of the image + */ + constructor(author: string | null, source: string | null, url: string) { + this.source = { + author, + authorUrl: null, + source, + sourceUrl: null, + imageDownloadUrl: url, // URL used for downloading the image + imageLinkUrl: url, // URL used for linking back to the website of the image + }; + + // extract the name from the url + this.name = Utils.fileName(this.source.imageDownloadUrl); + this.id = `${this.timestamp}_${this.name}`; + this.path = `${_wallpaperLocation}/${this.id}`; + } +} + +/** + * Controls the history and related code parts. + */ +class HistoryController { + history: HistoryEntry[] = []; + size = 10; + + private _logger = new Logger('RWG3', 'HistoryController'); + private _settings = new Settings(); + + /** + * Create a new HistoryController. + * + * Loads an existing history from the settings schema. + * + * @param {string} wallpaperLocation Root save location for new HistoryEntries. + */ + constructor(wallpaperLocation: string) { + _wallpaperLocation = wallpaperLocation; + + this.load(); + } + + /** + * Insert images at the beginning of the history. + * + * Throws old images out of the stack and saves to the schema. + * + * @param {HistoryEntry[]} historyElements Array of elements to insert + */ + insert(historyElements: HistoryEntry[]): void { + for (const historyElement of historyElements) + this.history.unshift(historyElement); + + this._deleteOldPictures(); + this.save(); + } + + /** + * Set the given id to to the first history element (the current one) + * + * @param {string} id ID of the historyEntry + * @returns {boolean} Whether the sorting was successful + */ + promoteToActive(id: string): boolean { + const element = this.get(id); + if (element === null) + return false; + + + element.timestamp = new Date().getTime(); + this.history = this.history.sort((elem1, elem2) => { + return elem1.timestamp < elem2.timestamp ? 1 : 0; + }); + this.save(); + + return true; + } + + /** + * Get a specific HistoryEntry by ID. + * + * @param {string} id ID of the HistoryEntry + * @returns {HistoryEntry | null} The corresponding HistoryEntry or null + */ + get(id: string): HistoryEntry | null { + for (const elem of this.history) { + if (elem.id === id) + return elem; + } + + return null; + } + + /** + * Get the current history element. + * + * @returns {HistoryEntry} Current first entry + */ + getCurrentEntry(): HistoryEntry { + return this.history[0]; + } + + /** + * Get a HistoryEntry by its file path. + * + * @param {string} path Path to search for + * @returns {HistoryEntry | null} The corresponding HistoryEntry or null + */ + getEntryByPath(path: string): HistoryEntry | null { + for (const element of this.history) { + if (element.path === path) + return element; + } + + return null; + } + + /** + * Get a random HistoryEntry. + * + * @returns {HistoryEntry} Random entry + */ + getRandom(): HistoryEntry { + return this.history[Utils.getRandomNumber(this.history.length)]; + } + + /** + * Load the history from the schema + */ + load(): void { + this.size = this._settings.getInt('history-length'); + + const stringHistory: string[] = this._settings.getStrv('history'); + this.history = stringHistory.map((elem: string) => { + const unknownObject = JSON.parse(elem) as unknown; + if (!this._isHistoryEntry(unknownObject)) + throw new Error('Failed loading history data.'); + + return unknownObject; + }); + } + + /** + * Save the history to the schema + */ + save(): void { + const stringHistory = this.history.map(elem => { + return JSON.stringify(elem); + }); + this._settings.setStrv('history', stringHistory); + Gio.Settings.sync(); + } + + /** + * Clear the history and delete all photos except the current one. + * + * This function clears the cache folder, ignoring if the image appears in the history or not. + */ + clear(): void { + const firstHistoryElement = this.history[0]; + + if (firstHistoryElement) + this.history = [firstHistoryElement]; + + const directory = Gio.file_new_for_path(_wallpaperLocation); + const enumerator = directory.enumerate_children('', Gio.FileQueryInfoFlags.NONE, null); + + let fileInfo; + do { + fileInfo = enumerator.next_file(null); + + if (!fileInfo) + break; + + const id = fileInfo.get_name(); + + // ignore hidden files and first element + if (id[0] !== '.' && id !== firstHistoryElement.id) { + const deleteFile = Gio.file_new_for_path(_wallpaperLocation + id); + this._deleteFile(deleteFile); + } + } while (fileInfo); + + this.save(); + } + + /** + * Delete all pictures that have no slot in the history. + */ + private _deleteOldPictures(): void { + this.size = this._settings.getInt('history-length'); + while (this.history.length > this.size) { + const path = this.history.pop()?.path; + if (!path) + continue; + + const file = Gio.file_new_for_path(path); + this._deleteFile(file); + } + } + + /** + * Helper function to delete files. + * + * Has some special treatments factored in to ignore file not found issues + * when the parent path is available. + * + * @param {Gio.FilePrototype} file The file to delete + * @throws On any other error than Gio.IOErrorEnum.NOT_FOUND + */ + private _deleteFile(file: Gio.FilePrototype): void { + try { + file.delete(null); + } catch (error) { + /** + * Ignore deletion errors when the file doesn't exist but the parent path is accessible. + * This tries to avoid invalid states later on because we would have thrown here and therefore skip saving. + */ + if (file.get_parent()?.query_exists(null) && error instanceof GLib.Error && error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)) { + this._logger.warn(`Ignoring Gio.IOErrorEnum.NOT_FOUND: ${file.get_path() ?? 'undefined'}`); + return; + } + + throw error; + } + } + + /** + * Check if an object is a HistoryEntry. + * + * @param {unknown} object Object to check + * @returns {boolean} Whether the object is a HistoryEntry + */ + private _isHistoryEntry(object: unknown): object is HistoryEntry { + if (typeof object === 'object' && + object && + 'timestamp' in object && + typeof object.timestamp === 'number' && + 'id' in object && + typeof object.id === 'string' && + 'path' in object && + typeof object.path === 'string' + ) + return true; + + return false; + } +} + +export {HistoryEntry, HistoryController}; diff --git a/src/historyMenuElements.ts b/src/historyMenuElements.ts new file mode 100644 index 00000000..ef5813bb --- /dev/null +++ b/src/historyMenuElements.ts @@ -0,0 +1,513 @@ +import Clutter from 'gi://Clutter'; +import Cogl from 'gi://Cogl'; +import GdkPixbuf from 'gi://GdkPixbuf'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; +import St from 'gi://St'; + +// Legacy importing style for shell internal bindings not available in standard import format +// For correct typing use: 'InstanceType' +const PopupMenu = imports.ui.popupMenu; + +import * as HistoryModule from './history.js'; +import * as Settings from './settings.js'; +import * as Utils from './utils.js'; + +import {AFTimer as Timer} from './timer.js'; +import {Logger} from './logger.js'; + +// https://gjs.guide/guides/gjs/asynchronous-programming.html#promisify-helper +Gio._promisify(Gio.File.prototype, 'copy_async', 'copy_finish'); +Gio._promisify(Gio.File.prototype, 'replace_contents_bytes_async', 'replace_contents_finish'); + +// GJS style class extending +const HistoryElement = GObject.registerClass({ + GTypeName: 'HistoryElement', +}, class HistoryElement extends PopupMenu.PopupSubMenuMenuItem { + private _logger = new Logger('RWG3', 'HistoryElement'); + private _settings = new Settings.Settings(); + + private _prefixLabel; + private _container; + private _dateLabel; + private _previewActor: Clutter.Actor | null = null; + + protected _setAsWallpaperItem; + + historyId: string; + historyEntry: HistoryModule.HistoryEntry; + + /** + * Create a new menu element for a HistoryEntry. + * + * @param {object | undefined} unusedParams Unused params object from the PopupMenu.PopupSubMenuMenuItem + * @param {HistoryModule.HistoryEntry} historyEntry HistoryEntry this menu element serves + * @param {number} index Place in history + */ + constructor(unusedParams: object | undefined, historyEntry: HistoryModule.HistoryEntry, index: number) { + super('', false); + + this.historyEntry = historyEntry; + this.historyId = this.historyEntry.id; // extend the actor with the historyId + + const timestamp = this.historyEntry.timestamp; + const date = new Date(timestamp); + + const timeString = date.toLocaleTimeString(); + const dateString = date.toLocaleDateString(); + + const prefixText = `${String(index)}.`; + this._prefixLabel = new St.Label({ + text: prefixText, + style_class: 'rwg-history-index', + }); + + if (index === 0) { + this.label.text = 'Current Background'; + } else { + this.actor.insert_child_above(this._prefixLabel, this.label); + this.label.destroy(); + } + + this._container = new St.BoxLayout({ + vertical: true, + }); + + this._dateLabel = new St.Label({ + text: dateString, + style_class: 'rwg-history-date', + }); + this._container.add_child(this._dateLabel); + + const timeLabel = new St.Label({ + text: timeString, + style_class: 'rwg-history-time', + }); + this._container.add_child(timeLabel); + + if (index !== 0) + this.actor.insert_child_above(this._container, this._prefixLabel); + + this.menu.actor.add_style_class_name('rwg-history-element-content'); + + if (this.historyEntry.source !== null) { + if (this.historyEntry.source.author !== null && + this.historyEntry.source.authorUrl !== null) { + const authorItem = new PopupMenu.PopupMenuItem(`Image By: ${this.historyEntry.source.author}`); + authorItem.connect('activate', () => { + if (this.historyEntry.source.authorUrl) { + Utils.execCheck(['xdg-open', this.historyEntry.source.authorUrl]).catch(error => { + this._logger.error(error); + }); + } + }); + + this.menu.addMenuItem(authorItem); + } + + if (this.historyEntry.source.source !== null && + this.historyEntry.source.sourceUrl !== null) { + const sourceItem = new PopupMenu.PopupMenuItem(`Image From: ${this.historyEntry.source.source}`); + sourceItem.connect('activate', () => { + if (this.historyEntry.source.sourceUrl) { + Utils.execCheck(['xdg-open', this.historyEntry.source.sourceUrl]).catch(error => { + this._logger.error(error); + }); + } + }); + + this.menu.addMenuItem(sourceItem); + } + + const imageUrlItem = new PopupMenu.PopupMenuItem('Open Image In Browser'); + imageUrlItem.connect('activate', () => { + if (this.historyEntry.source.imageLinkUrl) { + Utils.execCheck(['xdg-open', this.historyEntry.source.imageLinkUrl]).catch(error => { + this._logger.error(error); + }); + } + }); + + this.menu.addMenuItem(imageUrlItem); + } else { + this.menu.addMenuItem(new PopupMenu.PopupMenuItem('Unknown source.')); + } + + const previewItem = new PopupMenu.PopupBaseMenuItem({can_focus: false, reactive: false}); + this.menu.addMenuItem(previewItem); + + this._setAsWallpaperItem = new PopupMenu.PopupMenuItem('Set As Wallpaper'); + this._setAsWallpaperItem.connect('activate', () => { + this.emit('activate', null); // Fixme: not sure what the second parameter should be. null seems to work fine for now. + }); + + if (index !== 0) { + // this.menu.addMenuItem(new PopupMenu.PopupBaseMenuItem({ can_focus: false, reactive: false })); // theme independent spacing + this.menu.addMenuItem(this._setAsWallpaperItem); + } + + const copyToFavorites = new PopupMenu.PopupMenuItem('Save For Later'); + copyToFavorites.connect('activate', () => { + this._saveImage().catch(error => { + this._logger.error(error); + }); + }); + this.menu.addMenuItem(copyToFavorites); + + // Static URLs can't block images (yet?) + if (this.historyEntry.adapter?.type !== Utils.SourceType.STATIC_URL) { + const blockImage = new PopupMenu.PopupMenuItem('Add To Blocklist'); + blockImage.connect('activate', () => { + this._addToBlocklist(); + }); + this.menu.addMenuItem(blockImage); + } + + /* + Load the image on first opening of the sub menu instead of during creation of the history list. + */ + this.menu.connect('open-state-changed', (_, open) => { + if (typeof open === 'boolean' && open) { + if (this._previewActor !== null) + return; + + if (!this.historyEntry.path) { + this._logger.error('Image path in entry not found'); + return; + } + + try { + const width = 270; // 270 looks good for the now fixed 350px menu width + // const width = this.menu.actor.get_width(); // This should be correct but gives different results per element? + const pixbuf = GdkPixbuf.Pixbuf.new_from_file_at_scale(this.historyEntry.path, width, -1, true); + const height = pixbuf.get_height(); + + const image = new Clutter.Image(); + const pixelFormat = pixbuf.get_has_alpha() ? Cogl.PixelFormat.RGBA_8888 : Cogl.PixelFormat.RGB_888; + image.set_data( + pixbuf.get_pixels(), + pixelFormat, + width, + height, + pixbuf.get_rowstride() + ); + this._previewActor = new Clutter.Actor({height, width}); + this._previewActor.set_content(image); + + previewItem.actor.add_actor(this._previewActor); + } catch (exception) { + this._logger.error(String(exception)); + } + } + }); + } + + /** + * Add an image to the blocking list. + * + * Uses the filename for distinction. + */ + private _addToBlocklist(): void { + if (!this.historyEntry.adapter?.id || this.historyEntry.adapter.id === '-1' || !this.historyEntry.name) { + this._logger.error('Image entry is missing information'); + return; + } + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${this.historyEntry.adapter.id}/`; + const generalSettings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); + const blockedFilenames = generalSettings.getStrv('blocked-images'); + + if (blockedFilenames.includes(this.historyEntry.name)) + return; + + blockedFilenames.push(this.historyEntry.name); + generalSettings.setStrv('blocked-images', blockedFilenames); + } + + /** + * Save the image to the favorites folder. + */ + private async _saveImage(): Promise { + if (!this.historyEntry.path || !this.historyEntry.name) + throw new Error('Image entry is missing information'); + + const sourceFile = Gio.File.new_for_path(this.historyEntry.path); + const targetFolder = Gio.File.new_for_path(this._settings.getString('favorites-folder')); + const targetFile = targetFolder.get_child(this.historyEntry.name); + const targetInfoFile = targetFolder.get_child(`${this.historyEntry.name}.json`); + + try { + if (!targetFolder.make_directory_with_parents(null)) + throw new Error('Could not create directories.'); + } catch (error) { + if (error instanceof GLib.Error && error.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.EXISTS)) // noop + this._logger.debug('Folder already exists.'); + else // escalate + throw error; + } + + // @ts-expect-error This function was rewritten by Gio._promisify + // eslint-disable-next-line @typescript-eslint/await-thenable + if (!await sourceFile.copy_async(targetFile, Gio.FileCopyFlags.NONE, GLib.PRIORITY_DEFAULT, null, null)) + throw new Error('Failed copying image.'); + + // https://gjs.guide/guides/gio/file-operations.html#writing-file-contents + // @ts-expect-error This function was rewritten by Gio._promisify + // eslint-disable-next-line @typescript-eslint/await-thenable + const [success, message]: [boolean, string] = await targetInfoFile.replace_contents_bytes_async( + new TextEncoder().encode(JSON.stringify(this.historyEntry.source, null, '\t')), + null, + false, + Gio.FileCreateFlags.NONE, + null); + + if (!success) + throw new Error(`Failed writing file contents: ${message}`); + } + + /** + * Prefix the menu label with a number. + * + * @param {number} index Number to prefix + */ + setIndex(index: number): void { + this._prefixLabel.set_text(`${String(index)}.`); + } +}); + +const CurrentImageElement = GObject.registerClass({ + GTypeName: 'CurrentImageElement', +}, class CurrentImageElement extends HistoryElement { + /** + * Create a new image element for the currently active wallpaper. + * + * @param {object | undefined} params Option object of PopupMenu.PopupSubMenuMenuItem + * @param {HistoryModule.HistoryEntry} historyEntry History entry this menu is for + */ + constructor(params: object | undefined, historyEntry: HistoryModule.HistoryEntry) { + super(params, historyEntry, 0); + + if (this._setAsWallpaperItem) + this._setAsWallpaperItem.destroy(); + } +}); + +/** + * Element for the "New Wallpaper" button and the remaining time for the auto fetch + * feature. + * The remaining time will only be displayed if the af-feature is activated. + */ +const NewWallpaperElement = GObject.registerClass({ + GTypeName: 'NewWallpaperElement', +}, +class NewWallpaperElement extends PopupMenu.PopupBaseMenuItem { + private _timer = Timer.getTimer(); + private _remainingLabel; + + /** + * Create a button for fetching new wallpaper + * + * @param {object | undefined} params Options object of PopupMenu.PopupBaseMenuItem + */ + constructor(params: object | undefined) { + super(params); + + const container = new St.BoxLayout({ + vertical: true, + }); + + const newWPLabel = new St.Label({ + text: 'New Wallpaper', + style_class: 'rwg-new-label', + }); + container.add_child(newWPLabel); + + this._remainingLabel = new St.Label({ + text: '1 minute remaining', + }); + container.add_child(this._remainingLabel); + + this.actor.add_child(container); + } + + /** + * Checks the AF-setting and shows/hides the remaining minutes section. + */ + show(): void { + if (this._timer.isEnabled()) { + const remainingMinutes = this._timer.remainingMinutes(); + const minutes = remainingMinutes % 60; + const hours = Math.floor(remainingMinutes / 60); + + let hoursText = hours.toString(); + hoursText += hours === 1 ? ' hour' : ' hours'; + let minText = minutes.toString(); + minText += minutes === 1 ? ' minute' : ' minutes'; + + if (hours >= 1) + this._remainingLabel.text = `... ${hoursText} and ${minText} remaining.`; + else + this._remainingLabel.text = `... ${minText} remaining.`; + + + this._remainingLabel.show(); + } else { + this._remainingLabel.hide(); + } + } +}); + +/** + * The status element in the Gnome Shell top panel bar. + */ +class StatusElement { + icon; + + /** + * Create a new menu status element. + */ + constructor() { + this.icon = new St.Icon({ + icon_name: 'preferences-desktop-wallpaper-symbolic', + style_class: 'system-status-icon', + }); + } + + /** + * Pulsate the icon opacity as a loading animation. + */ + startLoading(): void { + // FIXME: Don't know where this is defined + // @ts-expect-error Don't know where this is defined + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + this.icon.ease({ + opacity: 20, + duration: 1337, + mode: Clutter.AnimationMode.EASE_IN_OUT_SINE, + autoReverse: true, + repeatCount: -1, + }); + } + + /** + * Stop pulsating the icon opacity. + */ + stopLoading(): void { + this.icon.remove_all_transitions(); + this.icon.opacity = 255; + } +} + +/** + * The history section holding multiple history elements. + */ +class HistorySection extends PopupMenu.PopupMenuSection { + /** + * Cache HistoryElements for performance of long histories. + */ + private _historySectionCache = new Map>(); + private _historyCache: HistoryModule.HistoryEntry[] = []; + + /** + * Create a new history section. + */ + constructor() { + super(); + + this.actor = new St.ScrollView({ + hscrollbar_policy: Gtk.PolicyType.NEVER, + vscrollbar_policy: Gtk.PolicyType.AUTOMATIC, + }); + + this.actor.add_actor(this.box); + } + + /** + * Clear and rebuild the history element list using cached elements where possible. + * + * @param {HistoryModule.HistoryEntry[]} history History list to rebuild from. + * @param {(HistoryElement) => void} onEnter Function to call on menu element key-focus-in + * @param {(HistoryElement) => void} onLeave Function to call on menu element key-focus-out + * @param {(HistoryElement) => void} onSelect Function to call on menu element enter-event + */ + updateList( + history: HistoryModule.HistoryEntry[], + onEnter: (actor: InstanceType) => void, + onLeave: (actor: InstanceType) => void, + onSelect: (actor: InstanceType) => void + ): void { + if (this._historyCache.length <= 1) + this.removeAll(); // remove empty history element + + const existingHistoryElements = []; + + for (let i = 1; i < history.length; i++) { + const historyID = history[i].id; + + if (!historyID) + continue; + + let cachedHistoryElement = this._historySectionCache.get(historyID); + if (!cachedHistoryElement) { + cachedHistoryElement = new HistoryElement(undefined, history[i], i); + cachedHistoryElement.actor.connect('key-focus-in', onEnter); + cachedHistoryElement.actor.connect('key-focus-out', onLeave); + cachedHistoryElement.actor.connect('enter-event', onEnter); + + cachedHistoryElement.connect('activate', onSelect); + this._historySectionCache.set(historyID, cachedHistoryElement); + + this.addMenuItem(cachedHistoryElement, i - 1); + } else { + cachedHistoryElement.setIndex(i); + } + + existingHistoryElements.push(historyID); + } + + this._cleanupHistoryCache(existingHistoryElements); + this._historyCache = history; + } + + /** + * Cleanup the cache for entries not in $existingIDs. + * + * @param {string[]} existingIDs List with IDs that exists in the history + */ + private _cleanupHistoryCache(existingIDs: string[]): void { + const destroyIDs = Array.from(this._historySectionCache.keys()).filter(i => existingIDs.indexOf(i) === -1); + + destroyIDs.forEach(id => { + this._historySectionCache.get(id)?.destroy(); + this._historySectionCache.delete(id); + }); + } + + /** + * Clear and remove all history elements. + */ + clear(): void { + this._cleanupHistoryCache([]); + this.removeAll(); + this.addMenuItem( + new PopupMenu.PopupMenuItem('No recent wallpaper ...', { + activate: false, + hover: false, + style_class: 'rwg-recent-label', + can_focus: false, + }) + ); + + this._historyCache = []; + } +} + +export { + StatusElement, + NewWallpaperElement, + HistorySection, + CurrentImageElement, + HistoryElement +}; diff --git a/src/jsonPath.ts b/src/jsonPath.ts new file mode 100644 index 00000000..066612c3 --- /dev/null +++ b/src/jsonPath.ts @@ -0,0 +1,121 @@ +import * as Utils from './utils.js'; + +/** + * Access a simple json path expression of an object. + * Returns the accessed value or null if the access was not possible. + * Accepts predefined number values to access the same elements as previously + * and allows to override the use of these values. + * + * @param {unknown} inputObject A JSON object + * @param {string} inputString JSONPath to follow, see wiki for syntax + * @returns {[unknown, string]} Tuple with an object of unknown type and a chosen JSONPath string + */ +function getTarget(inputObject: unknown, inputString: string): [object: unknown, chosenPath: string] { + if (!inputObject) + return [null, '']; + + if (inputString.length === 0) + return [inputObject, inputString]; + + let startDot = inputString.indexOf('.'); + if (startDot === -1) + startDot = inputString.length; + + let keyString = inputString.slice(0, startDot); + const inputStringTail = inputString.slice(startDot + 1); + + const startParentheses = keyString.indexOf('['); + + if (startParentheses === -1) { + // Expect Object here + const targetObject = _getObjectMember(inputObject, keyString); + if (!targetObject) + return [null, '']; + + const [object, path] = getTarget(targetObject, inputStringTail); + return [object, inputString.slice(0, inputString.length - inputStringTail.length) + path]; + } else { + const indexString = keyString.slice(startParentheses + 1, keyString.length - 1); + keyString = keyString.slice(0, startParentheses); + + // Expect an Array at this point + const targetObject = _getObjectMember(inputObject, keyString); + if (!targetObject || !Array.isArray(targetObject)) + return [null, '']; + + // add special keywords here + switch (indexString) { + case '@random': { + const [chosenElement, chosenNumber] = _randomElement(targetObject); + const [object, path] = getTarget(chosenElement, inputStringTail); + return [object, inputString.slice(0, inputString.length - inputStringTail.length).replace('@random', String(chosenNumber)) + path]; + } + default: { + // expecting integer + const [object, path] = getTarget(targetObject[parseInt(indexString)], inputStringTail); + return [object, inputString.slice(0, inputString.length - inputStringTail.length) + path]; + } + } + } +} + +/** + * Check validity of the key string and return the target member or null. + * + * @param {object} inputObject JSON object + * @param {string} keyString Name of the key in the object + * @returns {unknown | null} Found object member or null + */ +function _getObjectMember(inputObject: object, keyString: string): unknown { + if (keyString === '$') + return inputObject; + + for (const [key, value] of Object.entries(inputObject)) { + if (key === keyString) + return value; + } + + return null; +} + +/** + * Returns the value of a random key of a given array. + * + * @param {Array} array Array with values + * @returns {[T, number]} Tuple with an array member and index of that member + */ +function _randomElement(array: Array): [T, number] { + const randomNumber = Utils.getRandomNumber(array.length); + return [array[randomNumber], randomNumber]; +} + +/** + * Replace '@random' according to an already resolved path. + * + * '@random' would yield different results so this makes sure the values stay + * the same as long as the path is identical. + * + * @param {string} randomPath Path containing '@random' to resolve + * @param {string} resolvedPath Path with resolved '@random' + * @returns {string} Input string with replaced '@random' + */ +function replaceRandomInPath(randomPath: string, resolvedPath: string): string { + if (!randomPath.includes('@random')) + return randomPath; + + let newPath = randomPath; + while (newPath.includes('@random')) { + const startRandom = newPath.indexOf('@random'); + + // abort if path is not equal up to this point + if (newPath.substring(0, startRandom) !== resolvedPath.substring(0, startRandom)) + break; + + const endParenthesis = resolvedPath.indexOf(']', startRandom); + newPath = newPath.replace('@random', resolvedPath.substring(startRandom, endParenthesis)); + } + + return newPath; +} + +export {getTarget, replaceRandomInPath}; diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 00000000..d2755cfb --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,167 @@ +// https://gitlab.gnome.org/GNOME/gjs/-/blob/master/doc/Logging.md + +import {Settings} from './settings.js'; + +// Generated code produces a no-shadow rule error +/* eslint-disable */ +enum LogLevel { + SILENT, + ERROR, + WARNING, + INFO, + DEBUG, +} +/* eslint-enable */ +type LogLevelStrings = keyof typeof LogLevel; + +/** + * + */ +class Logger { + private _prefix: string; + private _callingClass: string; + private _settings = new Settings(); + + /** + * Create a new logging helper. + * + * @param {string} prefix Custom string to prepend + * @param {string} callingClass Class this logger writes messages for + */ + constructor(prefix: string, callingClass: string) { + this._prefix = prefix; + this._callingClass = callingClass; + } + + /** + * Helper function to safely log to the console. + * + * @param {LogLevelStrings} level String representation of the selected log level + * @param {unknown} message Message to send, ideally an Error() or string + */ + private _log(level: LogLevelStrings, message: unknown): void { + let errorMessage = String(message); + + if (message instanceof Error) + errorMessage = message.message; + + // This logs messages with GLib.LogLevelFlags.LEVEL_MESSAGE + log(`${this._prefix} [${level}] >> ${this._callingClass} :: ${errorMessage}`); + + // Log stack trace if available + if (message instanceof Error && message.stack) + // This logs messages with GLib.LogLevelFlags.LEVEL_WARNING + logError(message); + } + + /** + * Get the log level selected by the user. + * + * @returns {LogLevel} Log level + */ + private _selectedLogLevel(): LogLevel { + return this._settings.getInt('log-level') as LogLevel; + } + + /** + * Log a DEBUG message. + * + * @param {unknown} message Message to send, ideally an Error() or string + */ + debug(message: unknown): void { + if (this._selectedLogLevel() < LogLevel.DEBUG) + return; + + this._log('DEBUG', message); + } + + /** + * Log an INFO message. + * + * @param {unknown} message Message to send, ideally an Error() or string + */ + info(message: unknown): void { + if (this._selectedLogLevel() < LogLevel.INFO) + return; + + this._log('INFO', message); + } + + /** + * Log a WARN message. + * + * @param {unknown} message Message to send, ideally an Error() or string + */ + warn(message: unknown): void { + if (this._selectedLogLevel() < LogLevel.WARNING) + return; + + this._log('WARNING', message); + } + + /** + * Log an ERROR message. + * + * @param {unknown} message Message to send, ideally an Error() or string + */ + error(message: unknown): void { + if (this._selectedLogLevel() < LogLevel.ERROR) + return; + + this._log('ERROR', message); + } +} + +/** + * Retrieve the human readable enum name. + * + * @param {LogLevel} level The mode to name + * @returns {string} Name + */ +function _getLogLevelName(level: LogLevel): string { + let name: string; + + switch (level) { + case LogLevel.SILENT: + name = 'Silent'; + break; + case LogLevel.ERROR: + name = 'Error'; + break; + case LogLevel.WARNING: + name = 'Warning'; + break; + case LogLevel.INFO: + name = 'Info'; + break; + case LogLevel.DEBUG: + name = 'Debug'; + break; + + default: + name = 'LogLevel name not found'; + break; + } + + return name; +} + +/** + * Get a list of human readable enum entries. + * + * @returns {string[]} Array with key names + */ +function getLogLevelNameList(): string[] { + const list: string[] = []; + + const values = Object.values(LogLevel).filter(v => !isNaN(Number(v))); + for (const i of values) + list.push(_getLogLevelName(i as LogLevel)); + + return list; +} + +export { + Logger, + getLogLevelNameList +}; diff --git a/src/manager/defaultWallpaperManager.ts b/src/manager/defaultWallpaperManager.ts new file mode 100644 index 00000000..c68bffd3 --- /dev/null +++ b/src/manager/defaultWallpaperManager.ts @@ -0,0 +1,102 @@ +import * as Utils from '../utils.js'; + +import {WallpaperManager} from './wallpaperManager.js'; +import {Logger} from '../logger.js'; +import type {Settings} from '../settings.js'; + +/** + * A general default wallpaper manager. + * + * Unable to handle multiple displays. + */ +class DefaultWallpaperManager extends WallpaperManager { + protected _logger = new Logger('RWG3', 'DefaultManager'); + + /** + * Sets the background image in light and dark mode. + * + * @param {string[]} wallpaperPaths Array of strings to image files, expects a single image only. + * @returns {Promise} Only resolves + */ + protected async _setBackground(wallpaperPaths: string[]): Promise { + // The default manager can't handle multiple displays + if (wallpaperPaths.length > 1) + this._logger.warn('Single handling manager called with multiple images!'); + + await DefaultWallpaperManager.setSingleBackground(`file://${wallpaperPaths[0]}`, this._backgroundSettings); + + return Promise.resolve(); + } + + /** + * Sets the lock screen image in light and dark mode. + * + * @param {string[]} wallpaperPaths Array of strings to image files, expects a single image only. + * @returns {Promise} Only resolves + */ + protected async _setLockScreen(wallpaperPaths: string[]): Promise { + // The default manager can't handle multiple displays + if (wallpaperPaths.length > 1) + this._logger.warn('Single handling manager called with multiple images!'); + + await DefaultWallpaperManager.setSingleLockScreen(`file://${wallpaperPaths[0]}`, this._backgroundSettings, this._screensaverSettings); + + return Promise.resolve(); + } + + /** + * Default fallback function to set a single image background. + * + * @param {string} wallpaperURI URI to image file + * @param {Settings} backgroundSettings Settings containing the background `picture-uri` key + * @returns {Promise} Only resolves + */ + static setSingleBackground(wallpaperURI: string, backgroundSettings: Settings): Promise { + if (Utils.isImageMerged(wallpaperURI)) + // merged wallpapers need mode "spanned" + backgroundSettings.setString('picture-options', 'spanned'); + else + // single wallpapers need mode "zoom" + backgroundSettings.setString('picture-options', 'zoom'); + + Utils.setPictureUriOfSettingsObject(backgroundSettings, wallpaperURI); + return Promise.resolve(); + } + + /** + *Default fallback function to set a single image lock screen. + * + * @param {string} wallpaperURI URI to image file + * @param {Settings} backgroundSettings Settings containing the background `picture-uri` key + * @param {Settings} screensaverSettings Settings containing the lock screen `picture-uri` key + * @returns {Promise} Only resolves + */ + static setSingleLockScreen(wallpaperURI: string, backgroundSettings: Settings, screensaverSettings: Settings): Promise { + if (Utils.isImageMerged(wallpaperURI)) + // merged wallpapers need mode "spanned" + screensaverSettings.setString('picture-options', 'spanned'); + else + // single wallpapers need mode "zoom" + screensaverSettings.setString('picture-options', 'zoom'); + + Utils.setPictureUriOfSettingsObject(screensaverSettings, wallpaperURI); + return Promise.resolve(); + } + + /** + * Check if a filename matches a merged wallpaper name. + * + * Merged wallpaper need special handling as these are single images + * but span across all displays. + * + * @param {string} _filename Unused naming to check + * @returns {boolean} Whether the image is a merged wallpaper + */ + static isImageMerged(_filename: string): boolean { + // This manager can't create merged wallpaper + return false; + } +} + + +export {DefaultWallpaperManager}; diff --git a/src/manager/externalWallpaperManager.ts b/src/manager/externalWallpaperManager.ts new file mode 100644 index 00000000..94db416f --- /dev/null +++ b/src/manager/externalWallpaperManager.ts @@ -0,0 +1,148 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +import * as Utils from '../utils.js'; + +import {DefaultWallpaperManager} from './defaultWallpaperManager.js'; +import {Mode, WallpaperManager} from './wallpaperManager.js'; + +/** + * Abstract base class for external manager to implement. + */ +abstract class ExternalWallpaperManager extends WallpaperManager { + public canHandleMultipleImages = true; + + protected static _command: string[] | null = null; + protected abstract readonly _possibleCommands: string[]; + + private _cancellable: Gio.Cancellable | null = null; + + /** + * Checks if the current manager is available in the `$PATH`. + * + * @returns {boolean} Whether the manager is found + */ + isAvailable(): boolean { + if (ExternalWallpaperManager._command !== null) + return true; + + for (const command of this._possibleCommands) { + const path = GLib.find_program_in_path(command); + + if (path) { + ExternalWallpaperManager._command = [path]; + break; + } + } + + return ExternalWallpaperManager._command !== null; + } + + /** + * Set the wallpapers for a given mode. + * + * @param {string[]} wallpaperPaths Array of paths to the desired wallpapers, should match the display count + * @param {Mode} mode Enum indicating what images to change + */ + async setWallpaper(wallpaperPaths: string[], mode: Mode): Promise { + if (wallpaperPaths.length < 1) + throw new Error('Empty wallpaper array'); + + // Cancel already running processes before setting new images + this._cancelRunning(); + + // Fallback to default manager, all currently supported external manager don't support setting single images + if (wallpaperPaths.length === 1 || (mode === Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT && wallpaperPaths.length === 2)) { + const promises = []; + + if (mode === Mode.BACKGROUND || mode === Mode.BACKGROUND_AND_LOCKSCREEN) + promises.push(DefaultWallpaperManager.setSingleBackground(`file://${wallpaperPaths[0]}`, this._backgroundSettings)); + + if (mode === Mode.LOCKSCREEN || mode === Mode.BACKGROUND_AND_LOCKSCREEN) + promises.push(DefaultWallpaperManager.setSingleLockScreen(`file://${wallpaperPaths[0]}`, this._backgroundSettings, this._screensaverSettings)); + + if (mode === Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT) { + if (wallpaperPaths.length < 2) + throw new Error('Not enough wallpaper'); + + // Half the images for the background + promises.push(DefaultWallpaperManager.setSingleBackground(`file://${wallpaperPaths[0]}`, this._backgroundSettings)); + // Half the images for the lock screen + promises.push(DefaultWallpaperManager.setSingleLockScreen(`file://${wallpaperPaths[1]}`, this._backgroundSettings, this._screensaverSettings)); + } + + await Promise.allSettled(promises); + return; + } + + /** + * Don't run these concurrently! + * External manager may need to shove settings around to circumvent the fact the manager writes multiple settings on its own. + * These are called in this fixed order so external manager can rely on functions ran previously. + */ + + if (mode === Mode.BACKGROUND || mode === Mode.BACKGROUND_AND_LOCKSCREEN) + await this._setBackground(wallpaperPaths); + + if (mode === Mode.LOCKSCREEN) + await this._setLockScreen(wallpaperPaths); + + if (mode === Mode.BACKGROUND_AND_LOCKSCREEN) + await this._setLockScreenAfterBackground(wallpaperPaths); + + if (mode === Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT) { + await this._setBackground(wallpaperPaths.slice(0, wallpaperPaths.length / 2)); + await this._setLockScreen(wallpaperPaths.slice(wallpaperPaths.length / 2)); + } + } + + /** + * Forcefully stop a previously started manager process. + */ + private _cancelRunning(): void { + if (this._cancellable === null) + return; + + this._logger.debug('Stopping manager process.'); + this._cancellable.cancel(); + this._cancellable = null; + } + + /** + * Wrapper around calling the program command together with arguments. + * + * @param {string[]} commandArguments Arguments to append + */ + protected async _runExternal(commandArguments: string[]): Promise { + // Cancel already running processes before starting new ones + this._cancelRunning(); + + if (!ExternalWallpaperManager._command || ExternalWallpaperManager._command.length < 1) + throw new Error('Command empty!'); + + // Needs a copy here + const command = ExternalWallpaperManager._command.concat(commandArguments); + + this._cancellable = new Gio.Cancellable(); + + this._logger.debug(`Running command: ${command.toString()}`); + await Utils.execCheck(command, this._cancellable); + + this._cancellable = null; + } + + /** + * Sync the lock screen to the background. + * + * This function exists to save compute time on identical background and lock screen images. + * + * @param {string[]} _wallpaperPaths Unused array of strings to image files + * @returns {Promise} Only resolves + */ + protected _setLockScreenAfterBackground(_wallpaperPaths: string[]): Promise { + Utils.setPictureUriOfSettingsObject(this._screensaverSettings, this._backgroundSettings.getString('picture-uri')); + return Promise.resolve(); + } +} + +export {ExternalWallpaperManager}; diff --git a/src/manager/hydraPaper.ts b/src/manager/hydraPaper.ts new file mode 100644 index 00000000..5a6d52b4 --- /dev/null +++ b/src/manager/hydraPaper.ts @@ -0,0 +1,94 @@ +import * as Utils from '../utils.js'; + +import {ExternalWallpaperManager} from './externalWallpaperManager.js'; +import {Logger} from '../logger.js'; + +/** + * Wrapper for HydraPaper using it as a manager. + */ +class HydraPaper extends ExternalWallpaperManager { + protected readonly _possibleCommands = ['hydrapaper', 'org.gabmus.hydrapaper']; + protected _logger = new Logger('RWG3', 'HydraPaper'); + + /** + * Sets the background image in light and dark mode. + * + * @param {string[]} wallpaperPaths Array of strings to image files + */ + protected async _setBackground(wallpaperPaths: string[]): Promise { + await this._createCommandAndRun(wallpaperPaths); + + // Manually set key for darkmode because that's way faster than merging two times the same images + Utils.setPictureUriOfSettingsObject(this._backgroundSettings, this._backgroundSettings.getString('picture-uri')); + } + + /** + * Sets the lock screen image in light and dark mode. + * + * @param {string[]} wallpaperPaths Array of strings to image files + */ + protected async _setLockScreen(wallpaperPaths: string[]): Promise { + // Remember keys, HydraPaper will change these + const tmpBackground = this._backgroundSettings.getString('picture-uri-dark'); + const tmpMode = this._backgroundSettings.getString('picture-options'); + + // Force HydraPaper to target a different resulting image by using darkmode + await this._createCommandAndRun(wallpaperPaths, true); + + this._screensaverSettings.setString('picture-options', 'spanned'); + Utils.setPictureUriOfSettingsObject(this._screensaverSettings, this._backgroundSettings.getString('picture-uri-dark')); + + // HydraPaper possibly changed these, change them back + this._backgroundSettings.setString('picture-uri-dark', tmpBackground); + this._backgroundSettings.setString('picture-options', tmpMode); + } + + /** + * Run HydraPaper in CLI mode. + * + * HydraPaper: + * - Saves merged images in the cache folder. + * - Sets `picture-option` to `spanned` + * - Sets `picture-uri` or `picture-uri-dark` depending on {@link darkmode} + * - Needs matching image path count and display count + * + * @param {string[]} wallpaperArray Array of image paths, should match the display count + * @param {boolean} darkmode Use darkmode, gives different image in cache path + */ + private async _createCommandAndRun(wallpaperArray: string[], darkmode: boolean = false): Promise { + let command = []; + + if (darkmode) + command.push('--darkmode'); + + // hydrapaper [--darkmode] --cli PATH PATH PATH + command.push('--cli'); + command = command.concat(wallpaperArray); + + await this._runExternal(command); + } + + /** + * Check if a filename matches a merged wallpaper name. + * + * Merged wallpaper need special handling as these are single images + * but span across all displays. + * + * @param {string} filename Naming to check + * @returns {boolean} Whether the image is a merged wallpaper + */ + static isImageMerged(filename: string): boolean { + const mergedWallpaperNames = [ + 'merged_wallpaper', + ]; + + for (const name of mergedWallpaperNames) { + if (filename.includes(name)) + return true; + } + + return false; + } +} + +export {HydraPaper}; diff --git a/src/manager/superPaper.ts b/src/manager/superPaper.ts new file mode 100644 index 00000000..d43e9435 --- /dev/null +++ b/src/manager/superPaper.ts @@ -0,0 +1,91 @@ +import * as Utils from '../utils.js'; + +import {ExternalWallpaperManager} from './externalWallpaperManager.js'; +import {Logger} from './../logger.js'; + +/** + * Wrapper for Superpaper using it as a manager. + */ +class Superpaper extends ExternalWallpaperManager { + protected readonly _possibleCommands = ['superpaper']; + protected _logger = new Logger('RWG3', 'Superpaper'); + + /** + * Sets the background image in light and dark mode. + * + * @param {string[]} wallpaperPaths Array of strings to image files + */ + // We don't need the settings object because Superpaper already set both picture-uri on it's own. + protected async _setBackground(wallpaperPaths: string[]): Promise { + await this._createCommandAndRun(wallpaperPaths); + } + + /** + * Sets the lock screen image in light and dark mode. + * + * @param {string[]} wallpaperPaths Array of strings to image files + */ + protected async _setLockScreen(wallpaperPaths: string[]): Promise { + // Remember keys, Superpaper will change these + const tmpBackground = this._backgroundSettings.getString('picture-uri'); + const tmpBackgroundDark = this._backgroundSettings.getString('picture-uri-dark'); + const tmpMode = this._backgroundSettings.getString('picture-options'); + + await this._createCommandAndRun(wallpaperPaths); + + this._screensaverSettings.setString('picture-options', 'spanned'); + Utils.setPictureUriOfSettingsObject(this._screensaverSettings, this._backgroundSettings.getString('picture-uri-dark')); + + // Superpaper possibly changed these, change them back + this._backgroundSettings.setString('picture-uri', tmpBackground); + this._backgroundSettings.setString('picture-uri-dark', tmpBackgroundDark); + this._backgroundSettings.setString('picture-options', tmpMode); + } + + // https://github.com/hhannine/superpaper/blob/master/docs/cli-usage.md + /** + * Run Superpaper in CLI mode. + * + * Superpaper: + * - Saves merged images alternating in `$XDG_CACHE_HOME/superpaper/temp/cli-{a,b}.png` + * - Sets `picture-option` to `spanned` + * - Always sets both `picture-uri` and `picture-uri-dark` options + * - Can use only single images + * + * @param {string[]} wallpaperArray Array of paths to the desired wallpapers, should match the display count, can be a single image + */ + private async _createCommandAndRun(wallpaperArray: string[]): Promise { + let command = []; + + // cspell:disable-next-line + command.push('--setimages'); + command = command.concat(wallpaperArray); + + await this._runExternal(command); + } + + /** + * Check if a filename matches a merged wallpaper name. + * + * Merged wallpaper need special handling as these are single images + * but span across all displays. + * + * @param {string} filename Naming to check + * @returns {boolean} Whether the image is a merged wallpaper + */ + static isImageMerged(filename: string): boolean { + const mergedWallpaperNames = [ + 'cli-a', + 'cli-b', + ]; + + for (const name of mergedWallpaperNames) { + if (filename.includes(name)) + return true; + } + + return false; + } +} + +export {Superpaper}; diff --git a/src/manager/wallpaperManager.ts b/src/manager/wallpaperManager.ts new file mode 100644 index 00000000..5e06dfb6 --- /dev/null +++ b/src/manager/wallpaperManager.ts @@ -0,0 +1,113 @@ +import {Logger} from '../logger.js'; +import {Settings} from './../settings.js'; + +// Generated code produces a no-shadow rule error +/* eslint-disable */ +enum Mode { + /** Only change the desktop background */ + BACKGROUND, + /** Only change the lock screen background */ + LOCKSCREEN, + /** Change the desktop and lock screen background to the same image. */ + // This allows for optimizations when processing images. + BACKGROUND_AND_LOCKSCREEN, + /** Change each - the desktop and lock screen background - to different images. */ + BACKGROUND_AND_LOCKSCREEN_INDEPENDENT, +} +/* eslint-enable */ + +/** + * Wallpaper manager is a base class for manager to implement. + */ +abstract class WallpaperManager { + public canHandleMultipleImages = false; + + protected abstract _logger: Logger; + protected _backgroundSettings = new Settings('org.gnome.desktop.background'); + protected _screensaverSettings = new Settings('org.gnome.desktop.screensaver'); + + /** + * Set the wallpapers for a given mode. + * + * @param {string[]} wallpaperPaths Array of paths to the desired wallpapers, should match the display count + * @param {Mode} mode Enum indicating what images to change + */ + async setWallpaper(wallpaperPaths: string[], mode: Mode = Mode.BACKGROUND): Promise { + if (wallpaperPaths.length < 1) + throw new Error('Empty wallpaper array'); + + const promises = []; + if (mode === Mode.BACKGROUND || mode === Mode.BACKGROUND_AND_LOCKSCREEN) + promises.push(this._setBackground(wallpaperPaths)); + + if (mode === Mode.LOCKSCREEN || mode === Mode.BACKGROUND_AND_LOCKSCREEN) + promises.push(this._setLockScreen(wallpaperPaths)); + + if (mode === Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT) { + if (wallpaperPaths.length < 2) + throw new Error('Not enough wallpaper'); + + // Half the images for the background + promises.push(this._setBackground(wallpaperPaths.slice(0, wallpaperPaths.length / 2))); + // Half the images for the lock screen + promises.push(this._setLockScreen(wallpaperPaths.slice(wallpaperPaths.length / 2))); + } + + await Promise.allSettled(promises); + } + + protected abstract _setBackground(wallpaperPaths: string[]): Promise; + protected abstract _setLockScreen(wallpaperPaths: string[]): Promise; +} + +/** + * Retrieve the human readable enum name. + * + * @param {Mode} mode The mode to name + * @returns {string} Name + */ +function _getModeName(mode: Mode): string { + let name: string; + + switch (mode) { + case Mode.BACKGROUND: + name = 'Background'; + break; + case Mode.LOCKSCREEN: + name = 'Lockscreen'; + break; + case Mode.BACKGROUND_AND_LOCKSCREEN: + name = 'Background and lockscreen'; + break; + case Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT: + name = 'Background and lockscreen independently'; + break; + + default: + name = 'Mode name not found'; + break; + } + + return name; +} + +/** + * Get a list of human readable enum entries. + * + * @returns {string[]} Array with key names + */ +function getModeNameList(): string[] { + const list: string[] = []; + + const values = Object.values(Mode).filter(v => !isNaN(Number(v))); + for (const i of values) + list.push(_getModeName(i as Mode)); + + return list; +} + +export { + WallpaperManager, + Mode, + getModeNameList +}; diff --git a/randomwallpaper@iflow.space/metadata.json b/src/metadata.json similarity index 77% rename from randomwallpaper@iflow.space/metadata.json rename to src/metadata.json index f17fe8ad..e12fcbf6 100644 --- a/randomwallpaper@iflow.space/metadata.json +++ b/src/metadata.json @@ -1,10 +1,10 @@ { - "shell-version": [ "40", "40.0", "40.1", "41", "42", "43" ], + "shell-version": [ "43", "44" ], "uuid": "randomwallpaper@iflow.space", "settings-schema": "org.gnome.shell.extensions.space.iflow.randomwallpaper", "name": "Random Wallpaper", "description": "Fetches a random wallpaper from an online source and sets it as desktop background. \nThe desktop background can be updated periodically or manually.", - "version": 29, - "semantic-version": "2.7.3", + "version": 32, + "semantic-version": "3.0.0", "url": "https://github.com/ifl0w/RandomWallpaperGnome3" } diff --git a/src/prefs.ts b/src/prefs.ts new file mode 100644 index 00000000..02f78621 --- /dev/null +++ b/src/prefs.ts @@ -0,0 +1,339 @@ +// Use legacy style importing to work around standard imports not available in files loaded by the shell, those can't be modules (yet) +// > Note that as of GNOME 44, neither GNOME Shell nor Extensions support ESModules, and must use GJS custom import scheme. +// https://gjs.guide/extensions/overview/imports-and-modules.html#imports-and-modules +// https://gjs-docs.gnome.org/gjs/esmodules.md +// > JS ERROR: Extension randomwallpaper@iflow.space: SyntaxError: import declarations may only appear at top level of a module +// For correct typing use: 'InstanceType' +const Adw = imports.gi.Adw; +const Gio = imports.gi.Gio; +const Gtk = imports.gi.Gtk; +const ExtensionUtils = imports.misc.extensionUtils; + +import type * as SettingsNamespace from './settings.js'; +import type * as UtilsNamespace from './utils.js'; +import type * as LoggerNamespace from './logger.js'; +import type * as SourceRowNamespace from './ui/sourceRow.js'; +import type {ExtensionMeta} from 'ExtensionMeta'; + +let Settings: typeof SettingsNamespace; +let Utils: typeof UtilsNamespace; +let Logger: typeof LoggerNamespace; +let SourceRow: typeof SourceRowNamespace.SourceRow; + +const Self = ExtensionUtils.getCurrentExtension(); + +/** + * Like `extension.js` this is used for any one-time setup like translations. + * + * @param {ExtensionMeta} unusedMeta - An extension meta object, https://gjs.guide/extensions/overview/anatomy.html#extension-meta-object + */ +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +function init(unusedMeta: ExtensionMeta): void { + // Convenience.initTranslations(); +} + +// https://gjs.guide/extensions/overview/anatomy.html#prefs-js +// The code in prefs.js will be executed in a separate Gtk process +// Here you will not have access to code running in GNOME Shell, but fatal errors or mistakes will be contained within that process. +// In this process you will be using the Gtk toolkit, not Clutter. + +// https://gjs.guide/extensions/development/preferences.html#preferences-window +// Gnome 42+ +/** + * This function is called when the preferences window is first created to fill + * the `Adw.PreferencesWindow`. + * + * This function will only be called by GNOME 42 and later. If this function is + * present, `buildPrefsWidget()` will NOT be called. + * + * @param {Adw.PreferencesWindow} window - The preferences window + */ +// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars +function fillPreferencesWindow(window: InstanceType): void { + window.set_default_size(600, 720); + // temporary fill window to prevent error message until modules are loaded + const tmpPage = new Adw.PreferencesPage(); + window.add(tmpPage); + + new RandomWallpaperSettings(window, tmpPage); +} + +/** + * Main settings class for everything related to the settings window. + */ +class RandomWallpaperSettings { + private _logger!: LoggerNamespace.Logger; + private _settings!: SettingsNamespace.Settings; + private _backendConnection!: SettingsNamespace.Settings; + + private _sources: string[] = []; + private _builder = new Gtk.Builder(); + private _saveDialog: InstanceType | undefined; + + /** + * Create a new ui settings class. + * + * Replaces the placeholder $tmpPage once the modules are loaded and the real pages are available. + * + * @param {Adw.PreferencesWindow} window Window to fill with settings + * @param {Adw.PreferencesPage} tmpPage Placeholder settings page to replace + */ + constructor(window: InstanceType, tmpPage: InstanceType) { + // Dynamically load own modules. This allows us to use proper ES6 Modules + this._importModules().then(() => { + window.remove(tmpPage); + + if (!Logger || !Settings || !Utils || !SourceRow) + throw new Error('Error with imports'); + + this._logger = new Logger.Logger('RWG3', 'RandomWallpaper.Settings'); + this._settings = new Settings.Settings(); + this._backendConnection = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION); + + this._backendConnection.setBoolean('pause-timer', true); + this._loadSources(); + + // this._builder.set_translation_domain(Self.metadata['gettext-domain']); + this._builder.add_from_file(`${Self.path}/ui/pageGeneral.ui`); + this._builder.add_from_file(`${Self.path}/ui/pageSources.ui`); + + import('./manager/wallpaperManager.js').then(module => { + const comboBackgroundType = this._builder.get_object>('combo_background_type'); + comboBackgroundType.model = Gtk.StringList.new(module.getModeNameList()); + this._settings.bind('change-type', + comboBackgroundType, + 'selected', + Gio.SettingsBindFlags.DEFAULT); + }).catch(error => { + this._logger.error(error); + }); + + const comboLogLevel = this._builder.get_object>('log_level'); + comboLogLevel.model = Gtk.StringList.new(Logger.getLogLevelNameList()); + this._settings.bind('log-level', + comboLogLevel, + 'selected', + Gio.SettingsBindFlags.DEFAULT); + + this._settings.bind('minutes', + this._builder.get_object('duration_minutes'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hours', + this._builder.get_object('duration_hours'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('auto-fetch', + this._builder.get_object('af_switch'), + 'enable-expansion', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('disable-hover-preview', + this._builder.get_object('disable_hover_preview'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('hide-panel-icon', + this._builder.get_object('hide_panel_icon'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('fetch-on-startup', + this._builder.get_object('fetch_on_startup'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('general-post-command', + this._builder.get_object('general_post_command'), + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('multiple-displays', + this._builder.get_object('enable_multiple_displays'), + 'active', + Gio.SettingsBindFlags.DEFAULT); + + this._bindButtons(); + this._bindHistorySection(window); + + window.connect('close-request', () => { + this._backendConnection.setBoolean('pause-timer', false); + }); + + window.add(this._builder.get_object('page_general')); + window.add(this._builder.get_object('page_sources')); + + this._sources.forEach(id => { + const sourceRow = new SourceRow(undefined, id); + this._builder.get_object>('sources_list').add(sourceRow); + + sourceRow.button_delete.connect('clicked', () => { + sourceRow.clearConfig(); + this._builder.get_object>('sources_list').remove(sourceRow); + Utils.removeItemOnce(this._sources, id); + this._saveSources(); + }); + }); + + import('./utils.js').then(module => { + const manager = module.getWallpaperManager(); + if (manager.canHandleMultipleImages) + this._builder.get_object>('multiple_displays_row').set_sensitive(true); + }).catch(error => { + this._logger.error(error); + }); + }).catch(error => { + if (error instanceof Error) + logError(error); + else + logError(new Error('Unknown error')); + }); + } + + /** + * Import helper function. + * + * Loads all required modules async. + * This allows to omit the legacy GJS style imports (`const asd = imports.gi.asd`) + * and use proper modules for subsequent files. + * + * When the shell allows proper modules for loaded files (extension.js and prefs.js) + * this function can be removed and replaced by normal import statements. + */ + private async _importModules(): Promise { + const loggerPromise = import('./logger.js'); + const utilsPromise = import('./utils.js'); + const sourceRowPromise = import('./ui/sourceRow.js'); + const settingsPromise = import('./settings.js'); + + const [moduleLogger, moduleUtils, moduleSourceRow, moduleSettings] = await Promise.all( + [loggerPromise, utilsPromise, sourceRowPromise, settingsPromise]); + Logger = moduleLogger; + Utils = moduleUtils; + SourceRow = moduleSourceRow.SourceRow; + Settings = moduleSettings; + } + + /** + * Bind button clicks to logic. + */ + private _bindButtons(): void { + const newWallpaperButton: InstanceType = this._builder.get_object('request_new_wallpaper'); + const newWallpaperButtonLabel = newWallpaperButton.get_child() as InstanceType | null; + const origNewWallpaperText = newWallpaperButtonLabel?.get_label() ?? 'Request New Wallpaper'; + newWallpaperButton.connect('activated', () => { + newWallpaperButtonLabel?.set_label('Loading ...'); + newWallpaperButton.set_sensitive(false); + + // The backend sets this back to false after fetching the image - listen for that event. + const handler = this._backendConnection.observe('request-new-wallpaper', () => { + if (!this._backendConnection.getBoolean('request-new-wallpaper')) { + newWallpaperButtonLabel?.set_label(origNewWallpaperText); + newWallpaperButton.set_sensitive(true); + this._backendConnection.disconnect(handler); + } + }); + + this._backendConnection.setBoolean('request-new-wallpaper', true); + }); + + const sourceRowList = this._builder.get_object>('sources_list'); + this._builder.get_object('button_new_source').connect('clicked', () => { + const sourceRow = new SourceRow(); + sourceRowList.add(sourceRow); + this._sources.push(String(sourceRow.id)); + this._saveSources(); + + sourceRow.button_delete.connect('clicked', () => { + sourceRow.clearConfig(); + sourceRowList.remove(sourceRow); + Utils.removeItemOnce(this._sources, sourceRow.id); + this._saveSources(); + }); + }); + } + + /** + * Bind button clicks related to the history. + * + * @param {Adw.PreferencesWindow} window Preferences window + */ + private _bindHistorySection(window: InstanceType): void { + const entryRow = this._builder.get_object>('row_favorites_folder'); + entryRow.text = this._settings.getString('favorites-folder'); + + this._settings.bind('history-length', + this._builder.get_object('history_length'), + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('favorites-folder', + entryRow, + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._builder.get_object('clear_history').connect('clicked', () => { + this._backendConnection.setBoolean('clear-history', true); + }); + + this._builder.get_object('open_wallpaper_folder').connect('clicked', () => { + this._backendConnection.setBoolean('open-folder', true); + }); + + this._builder.get_object('button_favorites_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: window, + modal: true, + }); + + this._saveDialog.connect('response', (dialog, response_id) => { + // FIXME: ESLint complains about this comparison somehow? + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (response_id === Gtk.ResponseType.ACCEPT) { + const text = dialog.get_file()?.get_path(); + if (text) + entryRow.text = text; + } + dialog.destroy(); + }); + + this._saveDialog.show(); + }); + } + + /** + * Load the config from the schema + */ + private _loadSources(): void { + this._sources = this._settings.getStrv('sources'); + + // this._sources.sort((a, b) => { + // const path1 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${a}/`; + // const settingsGeneral1 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path1); + // const path2 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${b}/`; + // const settingsGeneral2 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path2); + + // const nameA = settingsGeneral1.get('name', 'string').toUpperCase(); + // const nameB = settingsGeneral2.get('name', 'string').toUpperCase(); + + // return nameA.localeCompare(nameB); + // }); + + this._sources.sort((a, b) => { + const path1 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${a}/`; + const settingsGeneral1 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path1); + const path2 = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${b}/`; + const settingsGeneral2 = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path2); + return settingsGeneral1.getInt('type') - settingsGeneral2.getInt('type'); + }); + } + + /** + * Save all configured sources to the settings. + */ + private _saveSources(): void { + this._settings.setStrv('sources', this._sources); + } +} diff --git a/src/randomWallpaperMenu.ts b/src/randomWallpaperMenu.ts new file mode 100644 index 00000000..12d2492b --- /dev/null +++ b/src/randomWallpaperMenu.ts @@ -0,0 +1,312 @@ +// These two rules contradict each other in TS and JS mode for @this in function descriptions below. +// @this can be removed in TS but then JS complains about missing @this in documentation. +// Disabling these rules for this specific file for now. +/* eslint-disable jsdoc/check-tag-names */ +/* eslint-disable jsdoc/valid-types */ + +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +// Legacy importing style for shell internal bindings not available in standard import format +// For correct typing use: 'InstanceType' +const ExtensionUtils = imports.misc.extensionUtils; +const Main = imports.ui.main; +const PanelMenu = imports.ui.panelMenu; +const PopupMenu = imports.ui.popupMenu; + +import * as CustomElements from './historyMenuElements.js'; +import * as Settings from './settings.js'; +import * as Utils from './utils.js'; + +import {Logger} from './logger.js'; +import {WallpaperController} from './wallpaperController.js'; +import {Mode} from './manager/wallpaperManager.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +/** + * PanelMenu for this extension. + */ +class RandomWallpaperMenu { + private _logger = new Logger('RWG3', 'RandomWallpaperEntry'); + private _backendConnection = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION); + private _savedBackgroundUri: string | null = null; + private _settings = new Settings.Settings(); + private _observedValues: number[] = []; + private _observedBackgroundValues: number[] = []; + + private _currentBackgroundSection; + private _historySection; + private _panelMenu; + private _wallpaperController; + + /** + * Create a new PanelMenu. + * + * @param {WallpaperController} wallpaperController The wallpaper controller controlling the wallpapers :D + */ + constructor(wallpaperController: WallpaperController) { + this._wallpaperController = wallpaperController; + + this._panelMenu = new PanelMenu.Button(0, 'Random wallpaper'); + + // PanelMenu Icon + const statusIcon = new CustomElements.StatusElement(); + this._panelMenu.add_child(statusIcon.icon); + this._observedValues.push(this._settings.observe('hide-panel-icon', this.updatePanelMenuVisibility.bind(this))); + + // new wallpaper button + const newWallpaperItem = new CustomElements.NewWallpaperElement({}); + this._panelMenu.menu.addMenuItem(newWallpaperItem); + + this._panelMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Set fixed width so the preview images don't widen the menu + this._panelMenu.menu.actor.set_width(350); + + // current background section + this._currentBackgroundSection = new PopupMenu.PopupMenuSection(); + this._panelMenu.menu.addMenuItem(this._currentBackgroundSection); + this._panelMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // history section + this._historySection = new CustomElements.HistorySection(); + this._panelMenu.menu.addMenuItem(this._historySection); + + this._panelMenu.menu.addMenuItem(new PopupMenu.PopupSeparatorMenuItem()); + + // Temporarily pause timer + const pauseTimerItem = new PopupMenu.PopupSwitchMenuItem('Pause timer', false); + pauseTimerItem.sensitive = this._settings.getBoolean('auto-fetch'); + pauseTimerItem.setToggleState(this._backendConnection.getBoolean('pause-timer')); + + pauseTimerItem.connect('toggled', (_, state: boolean) => { + this._backendConnection.setBoolean('pause-timer', state); + }); + + this._observedValues.push(this._settings.observe('auto-fetch', () => { + pauseTimerItem.sensitive = this._settings.getBoolean('auto-fetch'); + })); + + this._observedBackgroundValues.push(this._backendConnection.observe('pause-timer', () => { + pauseTimerItem.setToggleState(this._backendConnection.getBoolean('pause-timer')); + })); + + this._panelMenu.menu.addMenuItem(pauseTimerItem); + + // clear history button + const clearHistoryItem = new PopupMenu.PopupMenuItem('Clear History'); + this._panelMenu.menu.addMenuItem(clearHistoryItem); + + // open wallpaper folder button + const openFolder = new PopupMenu.PopupMenuItem('Open Wallpaper Folder'); + this._panelMenu.menu.addMenuItem(openFolder); + + // settings button + const openSettings = new PopupMenu.PopupMenuItem('Settings'); + this._panelMenu.menu.addMenuItem(openSettings); + + // add eventlistener + this._wallpaperController.registerStartLoadingHook(() => statusIcon.startLoading()); + this._wallpaperController.registerStopLoadingHook(() => statusIcon.stopLoading()); + this._wallpaperController.registerStopLoadingHook(() => this.setHistoryList()); + + // new wallpaper event + newWallpaperItem.connect('activate', () => { + // Make sure no other preview or reset event overwrites our setWallpaper! + this._wallpaperController.prohibitNewWallpaper = true; + this._wallpaperController.fetchNewWallpaper().then(() => { + }).catch(error => { + this._logger.error(error); + }).finally(() => { + this._wallpaperController.prohibitNewWallpaper = false; + }); + }); + + // clear history event + clearHistoryItem.connect('activate', () => { + this._wallpaperController.deleteHistory(); + }); + + // Open Wallpaper Folder + openFolder.connect('activate', () => { + const uri = GLib.filename_to_uri(this._wallpaperController.wallpaperLocation, ''); + Utils.execCheck(['xdg-open', uri]).catch(error => { + this._logger.error(error); + }); + }); + + openSettings.connect('activate', () => { + Gio.DBus.session.call( + 'org.gnome.Shell.Extensions', + '/org/gnome/Shell/Extensions', + 'org.gnome.Shell.Extensions', + 'OpenExtensionPrefs', + new GLib.Variant('(ssa{sv})', [Self.uuid, '', {}]), + null, + Gio.DBusCallFlags.NONE, + -1, + null).catch(error => { + this._logger.error(error); + }); + }); + + this._panelMenu.menu.connect('open-state-changed', (_, open) => { + if (open) { + // Save currently used background so we can reset to this + // in case only the lock screen was changed while the preview + // used the normal background + const backgroundSettings = new Settings.Settings('org.gnome.desktop.background'); + this._savedBackgroundUri = backgroundSettings.getString('picture-uri'); + + // Update remaining time label + newWallpaperItem.show(); + } else { + // Reset to the saved background image on popup closing + if (!this._wallpaperController.prohibitNewWallpaper && this._savedBackgroundUri) + this._wallpaperController.resetWallpaper(this._savedBackgroundUri); + + this._savedBackgroundUri = null; + } + }); + + // FIXME?: This triggers by leaving the underlying popupMenu and blocks previewing the items + // when entering from any side other than another item. (eg. spacer or the sides) + // this._panelMenu.menu.actor.connect('leave-event', () => { + // if (!this._wallpaperController.prohibitNewWallpaper) + // this._wallpaperController.resetWallpaper(this._savedBackgroundUri); + // }); + + this._observedValues.push(this._settings.observe('history', this.setHistoryList.bind(this))); + } + + /** + * Initialize remaining PanelMenu bits. + */ + init(): void { + this.updatePanelMenuVisibility(); + this.setHistoryList(); + + // add to panel + Main.panel.addToStatusArea('random-wallpaper-menu', this._panelMenu); + } + + /** + * Remove the PanelMenu and remnants. + */ + cleanup(): void { + this.clearHistoryList(); + this._panelMenu.destroy(); + + // remove all signal handlers + for (const observedValue of this._observedValues) + this._settings.disconnect(observedValue); + this._observedValues = []; + + for (const observedValue of this._observedBackgroundValues) + this._backendConnection.disconnect(observedValue); + this._observedBackgroundValues = []; + } + + /** + * Hide or show the PanelMenu based on user settings. + */ + updatePanelMenuVisibility(): void { + if (this._settings.getBoolean('hide-panel-icon')) + this._panelMenu.hide(); + else + this._panelMenu.show(); + } + + /** + * Recreates the current background section based on the history. + */ + setCurrentBackgroundElement(): void { + this._currentBackgroundSection.removeAll(); + + const historyController = this._wallpaperController.getHistoryController(); + const history = historyController.history; + + if (history.length > 0) { + const currentImage = new CustomElements.CurrentImageElement(undefined, history[0]); + this._currentBackgroundSection.addMenuItem(currentImage); + } + } + + /** + * Recreates the history list based on the history. + */ + setHistoryList(): void { + this._wallpaperController.update(); + this.setCurrentBackgroundElement(); + + const historyController = this._wallpaperController.getHistoryController(); + const history = historyController.history; + + if (history.length <= 1) { + this.clearHistoryList(); + return; + } + + /** + * Function for events that should happen on element leave. + * + * @param {InstanceType} unusedActor The activating panel item + * @this RandomWallpaperMenu + */ + function onLeave(this: RandomWallpaperMenu, unusedActor: InstanceType): void { + if (!this._wallpaperController.prohibitNewWallpaper && this._savedBackgroundUri) + this._wallpaperController.resetWallpaper(this._savedBackgroundUri); + } + + /** + * Function for events that should happen on element enter. + * + * @param {InstanceType} actor The activating panel item + * @this RandomWallpaperMenu + */ + function onEnter(this: RandomWallpaperMenu, actor: InstanceType): void { + if (!this._wallpaperController.prohibitNewWallpaper) + this._wallpaperController.previewWallpaper(actor.historyEntry.id); + } + + /** + * Function for events that should happen on element select. + * + * @param {InstanceType} actor The activating panel item + * @this RandomWallpaperMenu + */ + function onSelect(this: RandomWallpaperMenu, actor: InstanceType): void { + // Make sure no other preview or reset event overwrites our setWallpaper! + this._wallpaperController.prohibitNewWallpaper = true; + + this._wallpaperController.setWallpaper(actor.historyEntry.id).then(() => { + this._wallpaperController.prohibitNewWallpaper = false; + + if (this._settings.getInt('change-type') as Mode === Mode.LOCKSCREEN && this._savedBackgroundUri) { + // Reset background after previewing the lock screen options + this._wallpaperController.resetWallpaper(this._savedBackgroundUri); + } else { + // Update saved background with newly set background image + // so we don't revert to an older state when closing the menu + const backgroundSettings = new Settings.Settings('org.gnome.desktop.background'); + this._savedBackgroundUri = backgroundSettings.getString('picture-uri'); + } + }).catch(error => { + this._wallpaperController.prohibitNewWallpaper = false; + this._logger.error(error); + }); + } + + this._historySection.updateList(history, onEnter.bind(this), onLeave.bind(this), onSelect.bind(this)); + } + + /** + * Remove the history section + */ + clearHistoryList(): void { + this._historySection.clear(); + } +} + +export {RandomWallpaperMenu}; diff --git a/randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml b/src/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml similarity index 84% rename from randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml rename to src/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml index 243e3d2d..62535fde 100644 --- a/randomwallpaper@iflow.space/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml +++ b/src/schemas/org.gnome.shell.extensions.space.iflow.randomwallpaper.gschema.xml @@ -1,13 +1,6 @@ - - - - - - - @@ -56,12 +49,18 @@ A JS timestamp of the last timer callback trigger. Zero if no last change registered. - - "Background" + + 0 Choose what should be changed Allows to choose what backgrounds will be changed. + + 2 + Tier of logs + Choose what minimal tier of logs should appear in the journal. + + false Disable hover preview @@ -129,15 +128,6 @@ - - - - - - - - - @@ -158,21 +148,14 @@ Name for this source. - - "Unsplash" + + 0 Type The type of this source. - - - - - - - @@ -211,8 +194,8 @@ This results in a smaller wallpaper pool but the images are considered to have higher quality. - - 'Unconstraint' + + 0 Constraint Type The constraint of the Unsplash Source API. @@ -227,46 +210,52 @@ + + false + AI Art + Whether AI generated images should be included. + + false NSFW - Weather not safe images are allowed. + Whether not safe images are allowed. true SFW - Weather safe images are allowed. + Whether safe images are allowed. false Sketchy - Weather sketchy images are allowed. + Whether sketchy images are allowed. "" Api key - The wallheven api key. + The Wallhaven API key. true Category Anime - Weather the anime category should be searched. + Whether the anime category should be searched. true Category General - Weather the general category should be searched. + Whether the general category should be searched. true Category People - Weather the people category should be searched. + Whether the people category should be searched. @@ -281,10 +270,16 @@ The keyword will be used to search images. - - "1920x1200, 1920x1080, 2560x1440, 2560x1600, 3840x1080" - Resolutions - The acceptable resolutions. + + "1920x1080" + Minimal Resolution + The least acceptable resolution. + + + + "16x9,16x10" + Aspect ratios + List of acceptable aspect ratios. @@ -299,26 +294,26 @@ 16 - Image Ratio - The minimal ratio 1. + Image width ratio + The minimal width ratio part. 10 - Image Width - The minimal ratio 2. + Image height ratio + The minimal height ratio part. 1080 Image Height - The minimal height of fetched image. + The minimal height of the fetched image. 1920 Image Width - The minimal width of fetched image. + The minimal width of the fetched image. @@ -405,6 +400,12 @@ The URL to fetch. + + false + Different images + Yields different images on consecutive requests in a short amount of time. + + "" Domain diff --git a/src/settings.ts b/src/settings.ts new file mode 100644 index 00000000..1f8fef8d --- /dev/null +++ b/src/settings.ts @@ -0,0 +1,317 @@ +import Gio from 'gi://Gio'; +import GObject from 'gi://GObject'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +const Self = ExtensionUtils.getCurrentExtension(); + +const RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.backend-connection'; +const RWG_SETTINGS_SCHEMA_SOURCES_GENERAL = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.general'; +const RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.genericJSON'; +const RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.localFolder'; +const RWG_SETTINGS_SCHEMA_SOURCES_REDDIT = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.reddit'; +const RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.unsplash'; +const RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.urlSource'; +const RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN = 'org.gnome.shell.extensions.space.iflow.randomwallpaper.sources.wallhaven'; + +const RWG_SETTINGS_SCHEMA_PATH = '/org/gnome/shell/extensions/space-iflow-randomwallpaper'; + +/** + * Wrapper around gnome settings. + */ +class Settings { + private _settings: Gio.Settings; + + /** + * Create a new settings object. + * + * Will default to the general extension settings. + * + * @param {string | undefined} schemaId Schema ID or undefined, defaults to the extension schema ID + * @param {string | undefined} schemaPath Schema path or undefined + */ + constructor(schemaId?: string, schemaPath?: string) { + if (schemaPath === undefined) { + this._settings = ExtensionUtils.getSettings(schemaId); + } else { + // ExtensionUtils.getSettings() doesn't allow specifying a schema path + // We need the schema path to allow for account style settings (having the + // same settings schema id for multiple similar but distinctive settings objects). + // So we have to rebuild the original getSettings() function to get the raw + // schema object and build the Gio.Settings on our own with the schema path. + const schemaObj = this._getSchema(schemaId); + + this._settings = new Gio.Settings({settings_schema: schemaObj, path: schemaPath}); + } + } + + /** + * Bind a settings key to a GObject property. + * + * A GObject can only bind to one setting at a time. + * See observe() for one-way tracking with multiple watchers. + * + * @param {string} keyName Name of the setting key + * @param {GObject.Object} gObject GObject to bind to + * @param {string} property Name of the GObject property to bind to + * @param {Gio.SettingsBindFlags} settingsBindFlags Flags + */ + bind(keyName: string, gObject: GObject.Object, property: string, settingsBindFlags: Gio.SettingsBindFlags): void { + this._settings.bind(keyName, gObject, property, settingsBindFlags); + } + + /** + * Disconnect a watcher initiated by observe(). + * + * @param {number} handler ID of the observer to disconnect + */ + disconnect(handler: number): void { + this._settings.disconnect(handler); + } + + /** + * Get a boolean saved in a key. + * + * @param {string} key Key to query + * @returns {boolean} The saved value + */ + getBoolean(key: string): boolean { + return this._settings.get_boolean(key); + } + + /** + * Get an Enum saved in a key. + * + * @param {string} key Key to query + * @returns {number} The saved value + */ + getEnum(key: string): number { + return this._settings.get_enum(key); + } + + /** + * Get a number saved in a key. + * + * @param {string} key Key to query + * @returns {number} The saved value + */ + getInt(key: string): number { + return this._settings.get_int(key); + } + + /** + * Get a number saved in a key. + * + * @param {string} key Key to query + * @returns {number} The saved value + */ + getInt64(key: string): number { + return this._settings.get_int64(key); + } + + /** + * Get a string saved in a key. + * + * @param {string} key Key to query + * @returns {string} The saved value + */ + getString(key: string): string { + return this._settings.get_string(key); + } + + /** + * Get a list of strings saved in a key. + * + * @param {string} key Key to query + * @returns {string[]} The saved value + */ + getStrv(key: string): string[] { + return this._settings.get_strv(key); + } + + /** + * Get the current settings schema. + * + * @returns {Gio.SettingsSchema} The schema in use + */ + getSchema(): Gio.SettingsSchema { + return this._settings.settings_schema; + } + + /** + * Check if the schema key is writable. + * + * @param {string} key Key to query + * @returns {boolean} Whether the key is writable + */ + isWritable(key: string): boolean { + return this._settings.is_writable(key); + } + + /** + * List all keys available in the schema. + * + * @returns {string[]} List of keys + */ + listKeys(): string[] { + return this._settings.list_keys(); + } + + /** + * Watch a setting for changes. + * + * @param {string} key Settings key to watch for changes + * @param {(...args: unknown[]) => unknown} callback Function to call on value changes + * @returns {number} Handler ID, use for disconnect + */ + observe(key: string, callback: (...args: unknown[]) => unknown): number { + return this._settings.connect(`changed::${key}`, callback); + } + + /** + * Resets a key to its default value affectively removing this key. + * + * @param {string} keyName Key to reset + */ + reset(keyName: string): void { + this._settings.reset(keyName); + } + + /** + * Reset a whole schema to its default value affectively removing this schema. + */ + resetSchema(): void { + for (const key of this._settings.settings_schema.list_keys()) + this.reset(key); + } + + /** + * Save a boolean to a key. + * + * @param {string} key Key to save in + * @param {boolean} value Value to save + */ + setBoolean(key: string, value: boolean): void { + if (this._settings.set_boolean(key, value)) + this._save(); + else + throw new Error(`Could not set ${key} (type: boolean) with the value ${String(value)}`); + } + + /** + * Save an Enum to a key. + * + * @param {string} key Key to save in + * @param {number} value Value to save + */ + setEnum(key: string, value: number): void { + if (this._settings.set_enum(key, value)) + this._save(); + else + throw new Error(`Could not set ${key} (type: number) with the value ${value}`); + } + + /** + * Save a number to a key. + * + * @param {string} key Key to save in + * @param {number} value Value to save + */ + setInt(key: string, value: number): void { + if (this._settings.set_int(key, value)) + this._save(); + else + throw new Error(`Could not set ${key} (type: number) with the value ${value}`); + } + + /** + * Save a number to a key. + * + * @param {string} key Key to save in + * @param {number} value Value to save + */ + setInt64(key: string, value: number): void { + if (this._settings.set_int64(key, value)) + this._save(); + else + throw new Error(`Could not set ${key} (type: number64) with the value ${value}`); + } + + /** + * Save a string to a key. + * + * @param {string} key Key to save in + * @param {string} value Value to save + */ + setString(key: string, value: string): void { + if (this._settings.set_string(key, value)) + this._save(); + else + throw new Error(`Could not set ${key} (type: string) with the value ${value}`); + } + + /** + * Save a list of strings to a key. + * + * @param {string} key Key to save in + * @param {string[]} value Value to save + */ + setStrv(key: string, value: string[]): void { + if (this._settings.set_strv(key, value)) + this._save(); + else + throw new Error(`Could not set ${key} (type: string[]) with the value "${value.toString()}"`); + } + + /** + * Sync the settings object to disk. + */ + private _save(): void { + Gio.Settings.sync(); // Necessary: http://stackoverflow.com/questions/9985140 + } + + /** + * Helper function to get the extension settings schema object. + * + * @param {string | undefined} schemaId Schema ID, defaults to the extension settings schema ID + * @returns {Gio.SettingsSchema} Settings schema object for the given ID + */ + private _getSchema(schemaId?: string): Gio.SettingsSchema { + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/gnome-43/js/misc/extensionUtils.js#L211 + if (!schemaId) + schemaId = Self.metadata['settings-schema']; + + // Expect USER extensions to have a schemas/ subfolder, otherwise assume a + // SYSTEM extension that has been installed in the same prefix as the shell + const schemaDir = Self.dir.get_child('schemas'); + let schemaSource; + const schemaPath = schemaDir.get_path(); + if (schemaDir.query_exists(null) && schemaPath !== null) { + schemaSource = Gio.SettingsSchemaSource.new_from_directory(schemaPath, + Gio.SettingsSchemaSource.get_default(), + false); + } else { + schemaSource = Gio.SettingsSchemaSource.get_default(); + } + + const schemaObj = schemaSource?.lookup(schemaId, true); + if (!schemaObj) + throw new Error(`Schema ${schemaId} could not be found for extension ${Self.metadata.uuid}. Please check your installation`); + + return schemaObj; + } +} + +export { + Settings, + RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION, + RWG_SETTINGS_SCHEMA_PATH, + RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, + RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON, + RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER, + RWG_SETTINGS_SCHEMA_SOURCES_REDDIT, + RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH, + RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE, + RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN +}; diff --git a/src/soupBowl.ts b/src/soupBowl.ts new file mode 100644 index 00000000..f86373f1 --- /dev/null +++ b/src/soupBowl.ts @@ -0,0 +1,99 @@ +import GLib from 'gi://GLib'; +import Soup from 'gi://Soup'; + +import {Logger} from './logger.js'; + +/** + * A compatibility and convenience wrapper around the Soup API. + * + * libSoup is accessed through the SoupBowl wrapper to support libSoup3 and libSoup2.4 simultaneously in the extension + * runtime and in the preferences window. + */ +class SoupBowl { + MessageFlags = Soup.MessageFlags; + + private _logger = new Logger('RWG3', 'BaseAdapter'); + private _session = new Soup.Session(); + + /** + * Send a request with Soup. + * + * @param {Soup.Message} soupMessage Message to send + * @returns {Promise} Raw byte answer + */ + send_and_receive(soupMessage: Soup.Message): Promise { + if (Soup.get_major_version() === 2) + return this._send_and_receive_soup24(soupMessage); + else if (Soup.get_major_version() === 3) + return this._send_and_receive_soup30(soupMessage); + else + throw new Error('Unknown libsoup version'); + } + + /** + * Craft a new GET request. + * + * @param {string} uri Request address + * @returns {Soup.Message} Crafted message + */ + newGetMessage(uri: string): Soup.Message { + return Soup.Message.new('GET', uri); + } + + // Possibly wrong version here causing ignores to type checks + /** + * Send a request using Soup 2.4 + * + * @param {Soup.Message} soupMessage Request message + * @returns {Promise} Raw byte answer + */ + private _send_and_receive_soup24(soupMessage: Soup.Message): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + this._session.queue_message(soupMessage, (session, msg) => { + if (!msg.response_body) { + reject(new Error('Message has no response body')); + return; + } + + const response_body_bytes = msg.response_body.flatten().get_data(); + resolve(response_body_bytes); + }); + }); + } + + // Possibly wrong version here causing ignores to type checks + /** + * Send a request using Soup 3.0 + * + * @param {Soup.Message} soupMessage Request message + * @returns {Promise} Raw byte answer + */ + private _send_and_receive_soup30(soupMessage: Soup.Message): Promise { + return new Promise((resolve, reject) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + this._session.send_and_read_async(soupMessage, 0, null, (session: Soup.Session, message: Soup.Message) => { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const res_data = session.send_and_read_finish(message) as GLib.Bytes | null; + if (!res_data) { + reject(new Error('Message has no response body')); + return; + } + + const response_body_bytes = res_data.get_data(); + + if (response_body_bytes) + resolve(response_body_bytes); + else + reject(new Error('Empty response')); + }); + }); + } +} + +export {SoupBowl}; diff --git a/randomwallpaper@iflow.space/stylesheet.css b/src/stylesheet.css similarity index 77% rename from randomwallpaper@iflow.space/stylesheet.css rename to src/stylesheet.css index 1b2b452a..3f7ae199 100644 --- a/randomwallpaper@iflow.space/stylesheet.css +++ b/src/stylesheet.css @@ -1,8 +1,8 @@ -.rwg-new-lable { +.rwg-new-label { font-size: 130%; } -.rwg-recent-lable { +.rwg-recent-label { font-size: 90%; } @@ -20,5 +20,3 @@ .rwg-history-time { font-size: 80%; } - -.rwg-history-element-content { } diff --git a/src/timer.ts b/src/timer.ts new file mode 100644 index 00000000..40370bec --- /dev/null +++ b/src/timer.ts @@ -0,0 +1,238 @@ +import GLib from 'gi://GLib'; + +import {Logger} from './logger.js'; +import {Settings} from './settings.js'; + +/** + * Timer for the auto fetch feature. + */ +class AFTimer { + private static _afTimerInstance?: AFTimer | null = null; + + private _logger = new Logger('RWG3', 'Timer'); + private _settings = new Settings(); + private _timeout?: number = undefined; + private _timeoutEndCallback?: () => Promise = undefined; + private _minutes = 30; + private _paused = false; + + /** + * Get the timer singleton. + * + * @returns {AFTimer} Timer object + */ + static getTimer(): AFTimer { + if (!this._afTimerInstance) + this._afTimerInstance = new AFTimer(); + + return this._afTimerInstance; + } + + /** + * Remove the timer singleton. + */ + static destroy(): void { + if (this._afTimerInstance) + this._afTimerInstance.cleanup(); + + this._afTimerInstance = null; + } + + /** + * Continue a paused timer. + * + * Removes the pause lock and starts the timer. + * If the trigger time was surpassed while paused the callback gets + * called directly and the next trigger is scheduled at the + * next correct time frame repeatedly. + */ + continue(): void { + if (!this.isEnabled()) + return; + + this._logger.debug('Continuing timer'); + this._paused = false; + + // We don't care about awaiting. This should start immediately and + // run continuously in the background. + void this.start(); + } + + /** + * Check if the timer is currently set as enabled. + * + * @returns {boolean} Whether the timer is enabled + */ + isEnabled(): boolean { + return this._settings.getBoolean('auto-fetch'); + } + + /** + * Check if the timer is currently paused. + * + * @returns {boolean} Whether the timer is paused + */ + isPaused(): boolean { + return this._paused; + } + + /** + * Pauses the timer. + * + * This stops any currently running timer and prohibits starting + * until continue() was called. + * 'timer-last-trigger' stays the same. + */ + pause(): void { + this._logger.debug('Timer paused'); + this._paused = true; + this.cleanup(); + } + + /** + * Get the minutes until the timer activates. + * + * @returns {number} Minutes to activation + */ + remainingMinutes(): number { + const minutesElapsed = this.minutesElapsed(); + const remainder = minutesElapsed % this._minutes; + return Math.max(this._minutes - remainder, 0); + } + + /** + * Register a function which gets called on timer activation. + * + * Overwrites previously registered function. + * + * @param {() => Promise} callback Function to call + */ + registerCallback(callback: () => Promise): void { + this._timeoutEndCallback = callback; + } + + /** + * Sets the minutes of the timer. + * + * @param {number} minutes Number in minutes + */ + setMinutes(minutes: number): void { + this._minutes = minutes; + } + + /** + * Start the timer. + * + * Starts the timer if not paused. + * Removes any previously running timer. + * If the trigger time was surpassed the callback gets started + * directly and the next trigger is scheduled at the + * next correct time frame repeatedly. + * + * @param {boolean | undefined} forceTrigger Force calling the timeoutEndCallback on initial call + */ + async start(forceTrigger: boolean = false): Promise { + if (this._paused) + return; + + this.cleanup(); + + const last = this._settings.getInt64('timer-last-trigger'); + if (last === 0) + this._reset(); + + const millisecondsRemaining = this.remainingMinutes() * 60 * 1000; + + // set new wallpaper if the interval was surpassed… + const intervalSurpassed = this._surpassedInterval(); + if (forceTrigger || intervalSurpassed) { + if (this._timeoutEndCallback) { + this._logger.debug('Running callback now'); + + try { + await this._timeoutEndCallback(); + } catch (error) { + this._logger.error(error); + } + } + } + + // …and set the timestamp to when it should have been updated + if (intervalSurpassed) { + const millisecondsOverdue = (this._minutes * 60 * 1000) - millisecondsRemaining; + this._settings.setInt64('timer-last-trigger', Date.now() - millisecondsOverdue); + } + + // actual timer function + this._logger.debug(`Starting timer, will run callback in ${millisecondsRemaining}ms`); + this._timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, millisecondsRemaining, () => { + // Reset time immediately to avoid shifting the timer + this._reset(); + + // Call this function again and forcefully skip the surpassed timer check so it will run the timeoutEndCallback + this.start(true).catch(error => { + this._logger.error(error); + }); + + return GLib.SOURCE_REMOVE; + }); + } + + /** + * Stop the timer. + */ + stop(): void { + this._settings.setInt64('timer-last-trigger', 0); + this.cleanup(); + } + + /** + * Cleanup the timeout callback if it exists. + */ + cleanup(): void { + if (this._timeout) { // only remove if a timeout is active + this._logger.debug('Removing running timer'); + GLib.source_remove(this._timeout); + this._timeout = undefined; + } + } + + /** + * Sets the last activation time to [now]. This doesn't affect an already running timer. + */ + private _reset(): void { + this._settings.setInt64('timer-last-trigger', new Date().getTime()); + } + + /** + * Get the elapsed minutes since the last timer activation. + * + * @returns {number} Elapsed time in minutes + */ + minutesElapsed(): number { + const now = Date.now(); + const last = this._settings.getInt64('timer-last-trigger'); + + if (last === 0) + return 0; + + const elapsed = Math.max(now - last, 0); + return Math.floor(elapsed / (60 * 1000)); + } + + /** + * Checks if the configured timer interval has surpassed since the last timer activation. + * + * @returns {boolean} Whether the interval was surpassed + */ + private _surpassedInterval(): boolean { + const now = Date.now(); + const last = this._settings.getInt64('timer-last-trigger'); + const diff = now - last; + const intervalLength = this._minutes * 60 * 1000; + + return diff > intervalLength; + } +} + +export {AFTimer}; diff --git a/src/ui/genericJson.blp b/src/ui/genericJson.blp new file mode 100644 index 00000000..04e0589d --- /dev/null +++ b/src/ui/genericJson.blp @@ -0,0 +1,107 @@ +using Gtk 4.0; +using Adw 1; + +template $GenericJsonSettingsGroup : Adw.PreferencesGroup { + // title: _("Source Settings"); + description: _("This feature requires some know how. However, many different wallpaper providers can be used with this generic JSON source.\nYou have to specify an URL to a JSON response and a path to the target image URL within the JSON response.\nYou can also define a prefix that will be added to the image URL."); + + header-suffix: LinkButton { + valign: center; + uri: "https://github.com/ifl0w/RandomWallpaperGnome3/wiki/Generic-JSON-Source"; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + }; + + Adw.PreferencesGroup { + title: _("General"); + + Adw.EntryRow domain { + title: _("Domain"); + input-purpose: url; + + LinkButton { + valign: center; + uri: bind domain.text; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + } + } + + Adw.EntryRow request_url { + title: _("Request URL"); + input-purpose: url; + + LinkButton { + valign: center; + uri: bind request_url.text; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + } + } + } + + Adw.PreferencesGroup { + title: _("Image"); + + Adw.EntryRow image_path { + title: _("JSON Path"); + input-purpose: free_form; + } + + Adw.EntryRow image_prefix { + title: _("URL prefix"); + input-purpose: free_form; + } + } + + Adw.PreferencesGroup { + title: _("Post"); + + Adw.EntryRow post_path { + title: _("JSON Path"); + input-purpose: free_form; + } + + Adw.EntryRow post_prefix { + title: _("URL Prefix"); + input-purpose: free_form; + } + } + + Adw.PreferencesGroup { + title: _("Author"); + + Adw.EntryRow author_name_path { + title: _("Name JSON Path"); + input-purpose: free_form; + } + + Adw.EntryRow author_url_path { + title: _("URL JSON Path"); + input-purpose: free_form; + } + + Adw.EntryRow author_url_prefix { + title: _("URL prefix"); + input-purpose: free_form; + } + } +} diff --git a/src/ui/genericJson.ts b/src/ui/genericJson.ts new file mode 100644 index 00000000..cd88ea51 --- /dev/null +++ b/src/ui/genericJson.ts @@ -0,0 +1,101 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +const 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 { + // InternalChildren + private _author_name_path!: Adw.EntryRow; + private _author_url_path!: Adw.EntryRow; + private _author_url_prefix!: Adw.EntryRow; + private _domain!: Adw.EntryRow; + private _image_path!: Adw.EntryRow; + private _image_prefix!: Adw.EntryRow; + private _post_path!: Adw.EntryRow; + private _post_prefix!: Adw.EntryRow; + private _request_url!: Adw.EntryRow; + + private _settings; + + /** + * Craft a new adapter using an unique ID. + * + * Previously saved settings will be used if the adapter and ID match. + * + * @param {Partial | undefined} params Properties for Adw.PreferencesGroup or undefined + * @param {string} id Unique ID + */ + constructor(params: Partial | undefined, id: string) { + super(params); + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/genericJSON/${id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERIC_JSON, path); + + 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); + } + + /** + * Clear all config options associated to this specific adapter. + */ + clearConfig(): void { + this._settings.resetSchema(); + } +}); + +export {GenericJsonSettingsGroup}; diff --git a/src/ui/localFolder.blp b/src/ui/localFolder.blp new file mode 100644 index 00000000..973dc6a1 --- /dev/null +++ b/src/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: "folder-open-symbolic"; + } + } + } +} diff --git a/src/ui/localFolder.ts b/src/ui/localFolder.ts new file mode 100644 index 00000000..9e165ab9 --- /dev/null +++ b/src/ui/localFolder.ts @@ -0,0 +1,86 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +const 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 { + // InternalChildren + private _folder!: Gtk.Button; + private _folder_row!: Adw.EntryRow; + + private _saveDialog: Gtk.FileChooserNative | undefined; + private _settings; + + /** + * Craft a new adapter using an unique ID. + * + * Previously saved settings will be used if the adapter and ID match. + * + * @param {Partial | undefined} params Properties for Adw.PreferencesGroup or undefined + * @param {string} id Unique ID + */ + constructor(params: Partial | undefined, id: string) { + super(params); + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/localFolder/${id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_LOCAL_FOLDER, path); + + this._settings.bind('folder', + this._folder_row, + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._folder.connect('clicked', () => { + // TODO: 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() as Gtk.Window ?? undefined, + modal: true, + }); + + this._saveDialog.connect('response', (_dialog, response_id) => { + // FIXME: ESLint complains about this comparison somehow? + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (response_id === Gtk.ResponseType.ACCEPT) { + const chosenPath = _dialog.get_file()?.get_path(); + + if (chosenPath) + this._folder_row.text = chosenPath; + } + _dialog.destroy(); + }); + + this._saveDialog.show(); + }); + } + + /** + * Clear all config options associated to this specific adapter. + */ + clearConfig(): void { + this._settings.resetSchema(); + } +}); + +export {LocalFolderSettingsGroup}; diff --git a/src/ui/pageGeneral.blp b/src/ui/pageGeneral.blp new file mode 100644 index 00000000..e5661e4c --- /dev/null +++ b/src/ui/pageGeneral.blp @@ -0,0 +1,188 @@ +using Gtk 4.0; +using Adw 1; + +Adw.PreferencesPage page_general { + title: _("General"); + icon-name: "preferences-system-symbolic"; + + Adw.PreferencesGroup { + Adw.ActionRow request_new_wallpaper { + title: _("Request New Wallpaper"); + activatable: true; + + styles [ + "suggested-action", + "title-3", + ] + + // I don't know how to center the title so just overwrite it with a label + child: Label { + label: _("Request New Wallpaper"); + height-request: 50; + }; + } + } + + Adw.PreferencesGroup { + title: _("General Settings"); + + Adw.ComboRow combo_background_type { + title: _("Change type"); + use-subtitle: true; + } + + Adw.ActionRow { + title: _("Hide the panel icon"); + subtitle: _("You won't be able to access the history and the settings through the panel menu. Enabling this option currently is only reasonable in conjunction with the Auto-Fetching feature.\nOnly enable this option if you know how to open the settings without the panel icon!"); + + Switch hide_panel_icon { + valign: center; + } + } + + Adw.ActionRow { + title: _("Disable hover preview"); + subtitle: _("Disable the desktop preview of the background while hovering the history items. Try enabling if you encounter crashes or lags of the gnome-shell while using the extension."); + + Switch disable_hover_preview { + valign: center; + } + } + + Adw.EntryRow general_post_command { + title: _("Run post-command - available variables: %wallpaper_path%"); + } + + Adw.ActionRow multiple_displays_row { + title: _("Different wallpapers on multiple displays"); + subtitle: _("Requires HydraPaper or Superpaper.\nFills from History."); + sensitive: false; + + Switch enable_multiple_displays { + valign: center; + } + } + + Adw.ComboRow log_level { + title: _("Log level"); + subtitle: _("Set the tier of warnings appearing in the journal"); + } + } + + Adw.PreferencesGroup { + title: _("History"); + + header-suffix: Box { + spacing: 14; + + Button open_wallpaper_folder { + Adw.ButtonContent { + icon-name: "folder-open-symbolic"; + label: _("Open"); + } + + styles [ + "flat", + ] + } + + Button clear_history { + Adw.ButtonContent { + icon-name: "user-trash-symbolic"; + label: _("Delete"); + } + + styles [ + "destructive-action", + ] + } + }; + + Adw.ActionRow { + title: _("History length"); + subtitle: _("The number of wallpapers that will be shown in the history and stored in the wallpaper folder of this extension."); + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment history_length { + lower: 1; + upper: 100; + value: 10; + step-increment: 1; + page-increment: 10; + }; + } + } + + Adw.EntryRow row_favorites_folder{ + title: _("Save for later folder"); + + Button button_favorites_folder { + valign: center; + + Adw.ButtonContent { + icon-name: "folder-open-symbolic"; + } + } + } + } + + Adw.PreferencesGroup { + title: "Auto-Fetching"; + + Adw.ExpanderRow af_switch { + title: _("Auto-Fetching"); + subtitle: _("Automatically fetch new wallpapers based on an interval."); + show-enable-switch: true; + + Adw.ActionRow { + title: _("Hours"); + + Scale duration_slider_hours { + draw-value: true; + orientation: horizontal; + hexpand: true; + digits: 0; + + adjustment: Adjustment duration_hours { + value: 1; + step-increment: 1; + page-increment: 10; + lower: 0; + upper: 23; + }; + } + } + + Adw.ActionRow { + title: _("Minutes"); + + Scale duration_slider_minutes { + draw-value: true; + orientation: horizontal; + hexpand: true; + digits: 0; + + adjustment: Adjustment duration_minutes { + value: 30; + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 59; + }; + } + } + } + + Adw.ActionRow { + title: _("Fetch on startup (experimental)"); + subtitle: _("Fetch a new wallpaper during the startup of the extension. Rebooting your system, and enabling the extension will trigger a new wallpaper request.\nWARNING: Do not enable this feature if you observe crashes when requesting new wallpapers! This could render your system unstable as crashes could repeatedly happen on startup! In the case, you encounter such a problem, you will have to disable the extension or the feature manually from the commandline for your user."); + + Switch fetch_on_startup { + valign: center; + } + } + } +} diff --git a/src/ui/pageSources.blp b/src/ui/pageSources.blp new file mode 100644 index 00000000..d17af06c --- /dev/null +++ b/src/ui/pageSources.blp @@ -0,0 +1,22 @@ +using Gtk 4.0; +using Adw 1; + +Adw.PreferencesPage page_sources { + title: _("Wallpaper Sources"); + icon-name: "download-symbolic"; + + Adw.PreferencesGroup sources_list { + // title: _("Configured Wallpaper Sources"); + + header-suffix: Button button_new_source { + styles [ + "suggested-action", + ] + + Adw.ButtonContent { + icon-name: "add-symbolic"; + label: _("Add Source"); + } + }; + } +} diff --git a/src/ui/reddit.blp b/src/ui/reddit.blp new file mode 100644 index 00000000..f7e03e1b --- /dev/null +++ b/src/ui/reddit.blp @@ -0,0 +1,83 @@ +using Gtk 4.0; +using Adw 1; + +template $RedditSettingsGroup : Adw.PreferencesGroup { + title: _("General"); + + Adw.EntryRow subreddits { + title: _("Subreddits - e.g.: wallpaper, wallpapers, minimalwallpaper"); + } + + Adw.ActionRow { + title: _("Minimal resolution"); + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment min_width { + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 1000000; + }; + } + + Label { + label: "x"; + } + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment min_height { + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 1000000; + }; + } + } + + Adw.ActionRow { + title: _("Minimal image ratio"); + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment image_ratio1 { + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 1000000; + }; + } + + Label { + label: ":"; + } + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment image_ratio2 { + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 1000000; + }; + } + } + + Adw.ActionRow { + title: "SFW"; + subtitle: _("Safe for work"); + + Switch allow_sfw { + valign: center; + } + } +} diff --git a/src/ui/reddit.ts b/src/ui/reddit.ts new file mode 100644 index 00000000..6db01c49 --- /dev/null +++ b/src/ui/reddit.ts @@ -0,0 +1,84 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +const RedditSettingsGroup = GObject.registerClass({ + GTypeName: 'RedditSettingsGroup', + Template: GLib.filename_to_uri(`${Self.path}/ui/reddit.ui`, null), + InternalChildren: [ + 'allow_sfw', + 'image_ratio1', + 'image_ratio2', + 'min_height', + 'min_width', + 'subreddits', + ], +}, class RedditSettingsGroup extends Adw.PreferencesGroup { + // InternalChildren + private _allow_sfw!: Gtk.Switch; + private _image_ratio1!: Gtk.Adjustment; + private _image_ratio2!: Gtk.Adjustment; + private _min_height!: Gtk.Adjustment; + private _min_width!: Gtk.Adjustment; + private _subreddits!: Adw.EntryRow; + + private _settings; + + /** + * Craft a new adapter using an unique ID. + * + * Previously saved settings will be used if the adapter and ID match. + * + * @param {Partial | undefined} params Properties for Adw.PreferencesGroup or undefined + * @param {string} id Unique ID + */ + constructor(params: Partial | undefined, id: string) { + super(params); + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/reddit/${id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_REDDIT, path); + + this._settings.bind('allow-sfw', + this._allow_sfw, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-ratio1', + this._image_ratio1, + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-ratio2', + this._image_ratio2, + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('min-height', + this._min_height, + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('min-width', + this._min_width, + 'value', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('subreddits', + this._subreddits, + 'text', + Gio.SettingsBindFlags.DEFAULT); + } + + /** + * Clear all config options associated to this specific adapter. + */ + clearConfig(): void { + this._settings.resetSchema(); + } +}); + +export {RedditSettingsGroup}; diff --git a/src/ui/sourceRow.blp b/src/ui/sourceRow.blp new file mode 100644 index 00000000..4553389e --- /dev/null +++ b/src/ui/sourceRow.blp @@ -0,0 +1,70 @@ +using Gtk 4.0; +using Adw 1; + +template $SourceRow : Adw.ExpanderRow { + title: bind source_name.text; + show-enable-switch: true; + + // Doesn't look good and prone to missclicks + // [action] + // Button button_delete { + // valign: center; + + // styles [ + // "destructive-action", + // ] + + // Adw.ButtonContent { + // icon-name: "user-trash-symbolic"; + // valign: center; + // } + // } + + Box { + orientation: vertical; + spacing: 14; + + Adw.Clamp { + Adw.PreferencesGroup { + title: _("Meta"); + + Adw.EntryRow source_name { + title: _("Name"); + input-purpose: free_form; + text: _("My Source - (1080p)"); + } + + Adw.ComboRow combo { + title: _("Type"); + } + + Adw.ActionRow { + title: _("Delete this source"); + + Button button_delete { + valign: center; + + styles [ + "destructive-action", + ] + + Adw.ButtonContent { + icon-name: "user-trash-symbolic"; + valign: center; + } + } + } + + Adw.ExpanderRow blocked_images_list { + title: _("Blocked Images"); + sensitive: false; + } + } + } + + Adw.Clamp settings_container { } + + // Additional PreferencesGroup solely for spacing to the next row when expanded + Adw.PreferencesGroup { } + } +} diff --git a/src/ui/sourceRow.ts b/src/ui/sourceRow.ts new file mode 100644 index 00000000..b342cd70 --- /dev/null +++ b/src/ui/sourceRow.ts @@ -0,0 +1,209 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; +import * as Utils from './../utils.js'; + +import {Logger} from './../logger.js'; + +import {GenericJsonSettingsGroup} from './genericJson.js'; +import {LocalFolderSettingsGroup} from './localFolder.js'; +import {RedditSettingsGroup} from './reddit.js'; +import {UnsplashSettingsGroup} from './unsplash.js'; +import {UrlSourceSettingsGroup} from './urlSource.js'; +import {WallhavenSettingsGroup} from './wallhaven.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +// https://gitlab.gnome.org/GNOME/gjs/-/blob/master/examples/gtk4-template.js +const SourceRow = GObject.registerClass({ + GTypeName: 'SourceRow', + Template: GLib.filename_to_uri(`${Self.path}/ui/sourceRow.ui`, null), + Children: [ + 'button_delete', + ], + InternalChildren: [ + 'blocked_images_list', + 'combo', + 'settings_container', + 'source_name', + ], +}, class SourceRow extends Adw.ExpanderRow { + // This list is the same across all rows + static _stringList: Gtk.StringList; + + // Children + button_delete!: Gtk.Button; + + // InternalChildren + private _blocked_images_list!: Adw.ExpanderRow; + private _combo!: Adw.ComboRow; + private _settings_container!: Adw.Clamp; + private _source_name!: Adw.EntryRow; + + private _settings; + private _logger = new Logger('RWG3', 'SourceRow'); + + id = String(Date.now()); + + /** + * Craft a new source row using an unique ID. + * + * Default unique ID is Date.now() + * Previously saved settings will be used if the ID matches. + * + * @param {Partial | undefined} params Properties for Adw.ExpanderRow or undefined + * @param {string | null} id Unique ID or null + */ + constructor(params: Partial | undefined, id?: string | null) { + super(params); + + if (id) + this.id = id; + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${this.id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); + + if (!SourceRow._stringList) { + const availableTypeNames: string[] = []; + + // Fill combo from enum + // https://stackoverflow.com/a/39372911 + for (const type in Utils.SourceType) { + if (isNaN(Number(type))) + continue; + + availableTypeNames.push(Utils.getSourceTypeName(Number(type))); + } + + SourceRow._stringList = Gtk.StringList.new(availableTypeNames); + } + this._combo.model = SourceRow._stringList; + this._combo.selected = this._settings.getInt('type'); + + this._settings.bind('name', + this._source_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('enabled', + this, + 'enable-expansion', + Gio.SettingsBindFlags.DEFAULT); + + this._combo.connect('notify::selected', (comboRow: Adw.ComboRow) => { + this._settings.setInt('type', comboRow.selected); + this._fillRow(comboRow.selected); + }); + + this._fillRow(this._combo.selected); + + const blockedImages: string[] = this._settings.getStrv('blocked-images'); + blockedImages.forEach(filename => { + const blockedImageRow = new Adw.ActionRow(); + blockedImageRow.set_title(filename); + + const button = new Gtk.Button(); + button.set_valign(Gtk.Align.CENTER); + button.connect('clicked', () => { + this._removeBlockedImage(filename); + this._blocked_images_list.remove(blockedImageRow); + }); + + const buttonContent = new Adw.ButtonContent(); + buttonContent.set_icon_name('user-trash-symbolic'); + + button.set_child(buttonContent); + blockedImageRow.add_suffix(button); + this._blocked_images_list.add_row(blockedImageRow); + this._blocked_images_list.set_sensitive(true); + }); + } + + /** + * Fill this source row with adapter settings. + * + * @param {number} type Enum of the adapter to use + */ + private _fillRow(type: number): void { + const targetWidget = this._getSettingsGroup(type); + if (targetWidget !== null) + this._settings_container.set_child(targetWidget); + } + + /** + * Get a new adapter based on an enum source type. + * + * @param {Utils.SourceType} type Enum of the adapter to get + * @returns {object | null} Newly crafted adapter or null + */ + private _getSettingsGroup(type: Utils.SourceType = Utils.SourceType.UNSPLASH): GObject.RegisteredPrototype, { [key: string]: GObject.ParamSpec; }, unknown[]> + | GObject.RegisteredPrototype, { [key: string]: GObject.ParamSpec; }, unknown[]> + | GObject.RegisteredPrototype, { [key: string]: GObject.ParamSpec; }, unknown[]> + | GObject.RegisteredPrototype, { [key: string]: GObject.ParamSpec; }, unknown[]> + | GObject.RegisteredPrototype, { [key: string]: GObject.ParamSpec; }, unknown[]> + | GObject.RegisteredPrototype, { [key: string]: GObject.ParamSpec; }, unknown[]> + | null { + let targetWidget = null; + switch (type) { + case Utils.SourceType.UNSPLASH: + targetWidget = new UnsplashSettingsGroup(undefined, this.id); + break; + case Utils.SourceType.WALLHAVEN: + targetWidget = new WallhavenSettingsGroup(undefined, this.id); + break; + case Utils.SourceType.REDDIT: + targetWidget = new RedditSettingsGroup(undefined, this.id); + break; + case Utils.SourceType.GENERIC_JSON: + targetWidget = new GenericJsonSettingsGroup(undefined, this.id); + break; + case Utils.SourceType.LOCAL_FOLDER: + targetWidget = new LocalFolderSettingsGroup(undefined, this.id); + break; + case Utils.SourceType.STATIC_URL: + targetWidget = new UrlSourceSettingsGroup(undefined, this.id); + break; + default: + targetWidget = null; + this._logger.error('The selected source has no corresponding widget!'); + break; + } + return targetWidget; + } + + /** + * Remove an image name from the blocked image list. + * + * @param {string} filename Image name to remove + */ + private _removeBlockedImage(filename: string): void { + let blockedImages = this._settings.getStrv('blocked-images'); + if (!blockedImages.includes(filename)) + return; + + + blockedImages = Utils.removeItemOnce(blockedImages, filename); + this._settings.setStrv('blocked-images', blockedImages); + } + + /** + * Clear all keys associated to this ID across all adapter + */ + clearConfig(): void { + for (const i of Array(6).keys()) { + const widget = this._getSettingsGroup(i); + if (widget) + widget.clearConfig(); + } + + this._settings.resetSchema(); + } +}); + +export {SourceRow}; diff --git a/src/ui/unsplash.blp b/src/ui/unsplash.blp new file mode 100644 index 00000000..7efadf98 --- /dev/null +++ b/src/ui/unsplash.blp @@ -0,0 +1,64 @@ +using Gtk 4.0; +using Adw 1; + +template $UnsplashSettingsGroup : Adw.PreferencesGroup { + title: _("General"); + + Adw.EntryRow keyword { + title: _("Keywords - Comma seperated"); + input-purpose: free_form; + } + + Adw.ActionRow { + title: _("Only Featured Images"); + subtitle: _("This option results in a smaller image pool, but the images are considered to be of higher quality."); + + Switch featured_only { + valign: center; + } + } + + Adw.ActionRow { + title: _("Image Dimensions"); + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment image_width { + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 1000000; + }; + } + + Label { + label: "x"; + } + + SpinButton { + valign: center; + numeric: true; + + adjustment: Adjustment image_height { + step-increment: 1; + page-increment: 10; + lower: 1; + upper: 1000000; + }; + } + } + + Adw.PreferencesGroup { + title: _("Contraint"); + + Adw.ComboRow constraint_type { + title: _("Type"); + } + + Adw.EntryRow constraint_value { + title: _("Value"); + } + } +} diff --git a/src/ui/unsplash.ts b/src/ui/unsplash.ts new file mode 100644 index 00000000..b371dbe9 --- /dev/null +++ b/src/ui/unsplash.ts @@ -0,0 +1,116 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; +import {getConstraintTypeNameList} from '../adapter/unsplash.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +const 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', + ], +}, class UnsplashSettingsGroup extends Adw.PreferencesGroup { + // This list is the same across all rows + static _stringList: Gtk.StringList; + + // InternalChildren + private _constraint_type!: Adw.ComboRow; + private _constraint_value!: Adw.EntryRow; + private _featured_only!: Gtk.Switch; + private _image_height!: Gtk.Adjustment; + private _image_width!: Gtk.Adjustment; + private _keyword!: Adw.EntryRow; + + private _settings; + + /** + * Craft a new adapter using an unique ID. + * + * Previously saved settings will be used if the adapter and ID match. + * + * @param {Partial | undefined} params Properties for Adw.PreferencesGroup or undefined + * @param {string} id Unique ID + */ + constructor(params: Partial | undefined, id: string) { + super(params); + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/unsplash/${id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_UNSPLASH, path); + + if (!UnsplashSettingsGroup._stringList) + UnsplashSettingsGroup._stringList = Gtk.StringList.new(getConstraintTypeNameList()); + + this._constraint_type.model = UnsplashSettingsGroup._stringList; + + 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('featured-only', + this._featured_only, + 'active', + 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('keyword', + this._keyword, + '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: Adw.ComboRow) => { + this._unsplashUnconstrained(comboRow, true, this._featured_only); + this._unsplashUnconstrained(comboRow, false, this._constraint_value); + + this._featured_only.set_active(false); + }); + } + + /** + * Switch element sensitivity based on a selected combo row entry. + * + * @param {Adw.ComboRow} comboRow ComboRow with selected entry + * @param {boolean} enable Whether to make the element sensitive + * @param {Gtk.Widget} targetElement The element to target the sensitivity setting + */ + private _unsplashUnconstrained(comboRow: Adw.ComboRow, enable: boolean, targetElement: Gtk.Widget): void { + if (comboRow.selected === 0) + targetElement.set_sensitive(enable); + else + targetElement.set_sensitive(!enable); + } + + /** + * Clear all config options associated to this specific adapter. + */ + clearConfig(): void { + this._settings.resetSchema(); + } +}); + +export {UnsplashSettingsGroup}; diff --git a/src/ui/urlSource.blp b/src/ui/urlSource.blp new file mode 100644 index 00000000..90005f94 --- /dev/null +++ b/src/ui/urlSource.blp @@ -0,0 +1,84 @@ +using Gtk 4.0; +using Adw 1; + +template $UrlSourceSettingsGroup : Adw.PreferencesGroup { + Adw.PreferencesGroup { + title: _("General"); + + Adw.EntryRow domain { + title: _("Domain"); + input-purpose: url; + + LinkButton { + valign: center; + uri: bind domain.text; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + } + } + + Adw.EntryRow image_url { + title: _("Image URL"); + + LinkButton { + valign: center; + uri: bind image_url.text; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + } + } + + Adw.EntryRow post_url { + title: _("Post URL"); + input-purpose: free_form; + + LinkButton { + valign: center; + uri: bind post_url.text; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + } + } + + Adw.ActionRow { + title: _("Yields different images"); + subtitle: _("…on consecutive request in a short amount of time."); + + Switch different_images { + valign: center; + } + } + } + + Adw.PreferencesGroup { + title: _("Author"); + + Adw.EntryRow author_name { + title: _("Name"); + input-purpose: free_form; + } + + Adw.EntryRow author_url { + title: _("URL"); + input-purpose: free_form; + } + } +} diff --git a/src/ui/urlSource.ts b/src/ui/urlSource.ts new file mode 100644 index 00000000..3c2b9468 --- /dev/null +++ b/src/ui/urlSource.ts @@ -0,0 +1,84 @@ +import Adw from 'gi://Adw'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +const UrlSourceSettingsGroup = GObject.registerClass({ + GTypeName: 'UrlSourceSettingsGroup', + Template: GLib.filename_to_uri(`${Self.path}/ui/urlSource.ui`, null), + InternalChildren: [ + 'author_name', + 'author_url', + 'different_images', + 'domain', + 'image_url', + 'post_url', + ], +}, class UrlSourceSettingsGroup extends Adw.PreferencesGroup { + // InternalChildren + private _author_name!: Adw.EntryRow; + private _author_url!: Adw.EntryRow; + private _different_images!: Gtk.Switch; + private _domain!: Adw.EntryRow; + private _image_url!: Adw.EntryRow; + private _post_url!: Adw.EntryRow; + + private _settings; + + /** + * Craft a new adapter using an unique ID. + * + * Previously saved settings will be used if the adapter and ID match. + * + * @param {Partial | undefined} params Properties for Adw.PreferencesGroup or undefined + * @param {string} id Unique ID + */ + constructor(params: Partial | undefined, id: string) { + super(params); + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/urlSource/${id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_URL_SOURCE, path); + + this._settings.bind('author-name', + this._author_name, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('author-url', + this._author_url, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('different-images', + this._different_images, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('domain', + this._domain, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('image-url', + this._image_url, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('post-url', + this._post_url, + 'text', + Gio.SettingsBindFlags.DEFAULT); + } + + /** + * Clear all config options associated to this specific adapter. + */ + clearConfig(): void { + this._settings.resetSchema(); + } +}); + +export {UrlSourceSettingsGroup}; diff --git a/src/ui/wallhaven.blp b/src/ui/wallhaven.blp new file mode 100644 index 00000000..05cbafd0 --- /dev/null +++ b/src/ui/wallhaven.blp @@ -0,0 +1,138 @@ +using Gtk 4.0; +using Adw 1; + +template $WallhavenSettingsGroup : Adw.PreferencesGroup { + // title: _("Source Settings"); + + Adw.PreferencesGroup { + title: _("General"); + + Adw.EntryRow keyword { + title: _("Keywords - Comma seperated"); + input-purpose: free_form; + } + + Adw.PasswordEntryRow api_key { + title: _("API key"); + input-purpose: password; + + LinkButton { + valign: center; + uri: "https://wallhaven.cc/settings/account"; + + Adw.ButtonContent { + icon-name: "globe-symbolic"; + } + + styles [ + "flat", + ] + } + } + + Adw.EntryRow minimal_resolution { + title: _("Minimal resolution: 1920x1080"); + input-purpose: free_form; + text: ""; + } + + Adw.EntryRow aspect_ratios { + title: _("Allowed aspect ratios: 16x9,16x10"); + input-purpose: free_form; + text: ""; + } + + Adw.ActionRow row_color { + title: _("Search by color"); + subtitle: ""; + + Box { + Button button_color_undo { + valign: center; + + styles [ + "flat", + ] + + Adw.ButtonContent { + icon-name: "edit-undo-symbolic"; + } + } + + Button button_color { + valign: center; + + Adw.ButtonContent { + icon-name: "color-select-symbolic"; + } + } + } + } + + Adw.ActionRow { + title: _("Allow AI generated images"); + + Switch ai_art { + valign: center; + } + } + } + + Adw.PreferencesGroup { + title: _("Allowed content ratings"); + + Adw.ActionRow { + title: "SFW"; + subtitle: _("Safe for work"); + + Switch allow_sfw { + valign: center; + } + } + + Adw.ActionRow { + title: "Sketchy"; + + Switch allow_sketchy { + valign: center; + } + } + + Adw.ActionRow { + title: "NSFW"; + subtitle: _("Not safe for work"); + + Switch allow_nsfw { + valign: center; + } + } + } + + Adw.PreferencesGroup { + title: _("Categories"); + + Adw.ActionRow { + title: "General"; + + Switch category_general { + valign: center; + } + } + + Adw.ActionRow { + title: "Anime"; + + Switch category_anime { + valign: center; + } + } + + Adw.ActionRow { + title: "People"; + + Switch category_people { + valign: center; + } + } + } +} diff --git a/src/ui/wallhaven.ts b/src/ui/wallhaven.ts new file mode 100644 index 00000000..8805f0e5 --- /dev/null +++ b/src/ui/wallhaven.ts @@ -0,0 +1,181 @@ +import Adw from 'gi://Adw'; +import Gdk from 'gi://Gdk'; +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import GObject from 'gi://GObject'; +import Gtk from 'gi://Gtk'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as Settings from './../settings.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +const WallhavenSettingsGroup = GObject.registerClass({ + GTypeName: 'WallhavenSettingsGroup', + Template: GLib.filename_to_uri(`${Self.path}/ui/wallhaven.ui`, null), + InternalChildren: [ + 'ai_art', + 'allow_nsfw', + 'allow_sfw', + 'allow_sketchy', + 'api_key', + 'aspect_ratios', + 'button_color_undo', + 'button_color', + 'category_anime', + 'category_general', + 'category_people', + 'keyword', + 'minimal_resolution', + 'row_color', + ], +}, class WallhavenSettingsGroup extends Adw.PreferencesGroup { + private static _colorPalette: Gdk.RGBA[]; + private static _availableColors: string[] = [ + '#660000', '#990000', '#cc0000', '#cc3333', '#ea4c88', + '#993399', '#663399', '#333399', '#0066cc', '#0099cc', + '#66cccc', '#77cc33', '#669900', '#336600', '#666600', + '#999900', '#cccc33', '#ffff00', '#ffcc33', '#ff9900', + '#ff6600', '#cc6633', '#996633', '#663300', '#000000', + '#999999', '#cccccc', '#ffffff', '#424153', + ]; + + // InternalChildren + private _ai_art!: Gtk.Switch; + private _allow_nsfw!: Gtk.Switch; + private _allow_sfw!: Gtk.Switch; + private _allow_sketchy!: Gtk.Switch; + private _api_key!: Adw.EntryRow; + private _aspect_ratios!: Adw.EntryRow; + private _button_color_undo!: Gtk.Button; + private _button_color!: Gtk.Button; + private _category_anime!: Gtk.Switch; + private _category_general!: Gtk.Switch; + private _category_people!: Gtk.Switch; + private _keyword!: Adw.EntryRow; + private _minimal_resolution!: Adw.EntryRow; + private _row_color!: Adw.ActionRow; + + private _colorDialog: Gtk.ColorChooserDialog | undefined; + private _settings; + + /** + * Craft a new adapter using an unique ID. + * + * Previously saved settings will be used if the adapter and ID match. + * + * @param {Partial | undefined} params Properties for Adw.PreferencesGroup or undefined + * @param {string} id Unique ID + */ + constructor(params: Partial | undefined, id: string) { + super(params); + + const path = `${Settings.RWG_SETTINGS_SCHEMA_PATH}/sources/wallhaven/${id}/`; + this._settings = new Settings.Settings(Settings.RWG_SETTINGS_SCHEMA_SOURCES_WALLHAVEN, path); + + this._settings.bind('ai-art', + this._ai_art, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('allow-nsfw', + this._allow_nsfw, + '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('api-key', + this._api_key, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('category-anime', + this._category_anime, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('category-general', + this._category_general, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('category-people', + this._category_people, + 'active', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('color', + this._row_color, + 'subtitle', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('keyword', + this._keyword, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('minimal-resolution', + this._minimal_resolution, + 'text', + Gio.SettingsBindFlags.DEFAULT); + this._settings.bind('aspect-ratios', + this._aspect_ratios, + 'text', + Gio.SettingsBindFlags.DEFAULT); + + this._button_color_undo.connect('clicked', () => { + this._row_color.subtitle = ''; + }); + + this._button_color.connect('clicked', () => { + // TODO: For GTK 4.10+ + // Gtk.ColorDialog(); + + // https://stackoverflow.com/a/54487948 + this._colorDialog = new Gtk.ColorChooserDialog({ + title: 'Choose a Color', + transient_for: this.get_root() as Gtk.Window ?? undefined, + modal: true, + }); + this._colorDialog.set_use_alpha(false); + + if (!WallhavenSettingsGroup._colorPalette) { + WallhavenSettingsGroup._colorPalette = []; + + WallhavenSettingsGroup._availableColors.forEach(hexColor => { + const rgbaColor = new Gdk.RGBA(); + rgbaColor.parse(hexColor); + WallhavenSettingsGroup._colorPalette.push(rgbaColor); + }); + } + this._colorDialog.add_palette(Gtk.Orientation.HORIZONTAL, 10, WallhavenSettingsGroup._colorPalette); + + this._colorDialog.connect('response', (dialog, response_id) => { + // FIXME: ESLint complains about this comparison somehow? + // eslint-disable-next-line @typescript-eslint/no-unsafe-enum-comparison + if (response_id === Gtk.ResponseType.OK) { + // result is a Gdk.RGBA which uses float + const rgba = dialog.get_rgba(); + // convert to rgba so it's useful + const rgbaString = rgba.to_string(); // rgb(0,0,0) + const rgbaArray = rgbaString.replace('rgb(', '').replace(')', '').split(','); + const hexString = `${parseInt(rgbaArray[0]).toString(16).padStart(2, '0')}${parseInt(rgbaArray[1]).toString(16).padStart(2, '0')}${parseInt(rgbaArray[2]).toString(16).padStart(2, '0')}`; + this._row_color.subtitle = hexString; + } + dialog.destroy(); + }); + + this._colorDialog.show(); + }); + } + + /** + * Clear all config options associated to this specific adapter. + */ + clearConfig(): void { + this._settings.resetSchema(); + } +}); + +export {WallhavenSettingsGroup}; diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 00000000..87041254 --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,291 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; +import type Meta from 'gi://Meta'; + +import {DefaultWallpaperManager} from './manager/defaultWallpaperManager.js'; +import {HydraPaper} from './manager/hydraPaper.js'; +import {Logger} from './logger.js'; +import {Superpaper} from './manager/superPaper.js'; +import {Settings} from './settings.js'; +import {type WallpaperManager} from './manager/wallpaperManager.js'; + +// Generated code produces a no-shadow rule error: +// 'SourceType' is already declared in the upper scope on line 7 column 5 no-shadow +/* eslint-disable */ +enum SourceType { + UNSPLASH = 0, + WALLHAVEN, + REDDIT, + GENERIC_JSON, + LOCAL_FOLDER, + STATIC_URL, +} +/* eslint-enable */ + +/** + * Get the string representation of an enum SourceType. + * + * @param {SourceType} value The enum value to request + * @returns {string} Name of the corresponding source type + */ +function getSourceTypeName(value: SourceType): string { + switch (value) { + case SourceType.UNSPLASH: + return 'Unsplash'; + case SourceType.WALLHAVEN: + return 'Wallhaven'; + case SourceType.REDDIT: + return 'Reddit'; + case SourceType.GENERIC_JSON: + return 'Generic JSON'; + case SourceType.LOCAL_FOLDER: + return 'Local Folder'; + case SourceType.STATIC_URL: + return 'Static URL'; + + default: + return 'Unsplash'; + } +} + +/** + * Returns a promise which resolves cleanly or rejects according to the underlying subprocess. + * + * @param {string[]} argv String array of command and parameter + * @param {Gio.Cancellable} [cancellable] Object to cancel the command later in lifetime + */ +function execCheck(argv: string[], cancellable?: Gio.Cancellable | null): Promise { + let cancelId = 0; + const proc = new Gio.Subprocess({ + argv, + flags: Gio.SubprocessFlags.NONE, + }); + + // This does not take "undefined" despite the docs saying otherwise + proc.init(cancellable ?? null); + + if (cancellable instanceof Gio.Cancellable) + cancelId = cancellable.connect(() => proc.force_exit()); + + return new Promise((resolve, reject) => { + // This does not take "undefined" despite the docs saying otherwise + proc.wait_check_async(cancellable ?? null, (_proc, res) => { + if (_proc === null) { + reject(new Error('Failed getting process.')); + return; + } + + try { + if (!_proc.wait_check_finish(res)) { + const status = _proc.get_exit_status(); + + throw new Gio.IOErrorEnum({ + code: Gio.io_error_from_errno(status).code, + message: GLib.strerror(status), + }); + } + + resolve(); + } catch (e) { + reject(e); + } finally { + if (cancellable instanceof Gio.Cancellable && cancelId > 0) + cancellable.disconnect(cancelId); + } + }); + }); +} + +/** + * Retrieves the file name part of an URI + * + * @param {string} uri URI to scan + * @returns {string} Filename part + */ +function fileName(uri: string): string { + while (_isURIEncoded(uri)) + uri = decodeURIComponent(uri); + + let base = uri.substring(uri.lastIndexOf('/') + 1); + if (base.indexOf('?') >= 0) + base = base.substring(0, base.indexOf('?')); + + return base; +} + +// https://stackoverflow.com/a/32859917 +/** + * Compare two strings and return the first char they differentiate. + * + * Begins counting on 0 and returns -1 if the strings are identical. + * + * @param {string} str1 String to compare + * @param {string} str2 String to compare + * @returns {number} First different char or -1 + */ +function findFirstDifference(str1: string, str2: string): number { + let i = 0; + if (str1 === str2) + return -1; + while (str1[i] === str2[i]) + i++; + return i; +} + +/** + * Get the amount of currently connected displays. + * + * @returns {number} Connected display count + */ +function getMonitorCount(): number { + // FIXME: Figure out where the 'global' thing can be imported from + // @ts-expect-error Figure out where the 'global' thing can be imported from + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + const currentDisplay = global?.display as Meta.Display; + const count = currentDisplay?.get_n_monitors(); + + if (count) + return count; + + new Logger('RWG3', 'Utils').warn('Unable to get monitor count!'); + return 1; +} + +/** + * Get a random number between 0 and a given value. + * + * @param {number} size Maximum + * @returns {number} Random number between 0 and $size + */ +function getRandomNumber(size: number): number { + // https://stackoverflow.com/a/5915122 + return Math.floor(Math.random() * size); +} + +// https://stackoverflow.com/a/12646864 +/** + * Shuffle all entries in an array into random order. + * + * @param {T[]} array Array to shuffle + * @returns {T[]} Shuffled array + */ +function shuffleArray(array: T[]): T[] { + for (let i = array.length - 1; i > 0; i--) { + const j = getRandomNumber(i + 1); + [array[i], array[j]] = [array[j], array[i]]; + } + + return array; +} + +/** + * Check if a string is an URI. + * + * @param {string} uri The URI to check + * @returns {boolean} Whether the given string is an URI + */ +function _isURIEncoded(uri: string): boolean { + uri = uri || ''; + + return uri !== decodeURIComponent(uri); +} + +// https://stackoverflow.com/a/5767357 +/** + * Remove the first matching item of an array. + * + * @param {Array} array Array of items + * @param {T} value Item to remove + * @returns {Array} Array with first encountered item removed + */ +function removeItemOnce(array: T[], value: T): T[] { + const index = array.indexOf(value); + if (index > -1) + array.splice(index, 1); + + return array; +} + +/** + * Set the picture-uri property of the given settings object to the path. + * Precondition: the settings object has to be a valid Gio settings object with the picture-uri property. + * + * @param {Settings} settings The settings schema object containing the keys to change + * @param {string} uri The picture URI to be set + */ +function setPictureUriOfSettingsObject(settings: Settings, uri: string): void { + /* + * inspired from: + * https://bitbucket.org/LukasKnuth/backslide/src/7e36a49fc5e1439fa9ed21e39b09b61eca8df41a/backslide@codeisland.org/settings.js?at=master + */ + const setProp = (property: string): void => { + if (settings.isWritable(property)) { + // Set a new Background-Image (should show up immediately): + settings.setString(property, uri); + } else { + throw new Error(`Property not writable: ${property}`); + } + }; + + const availableKeys = settings.listKeys(); + + let property = 'picture-uri'; + if (availableKeys.indexOf(property) !== -1) + setProp(property); + + + property = 'picture-uri-dark'; + if (availableKeys.indexOf(property) !== -1) + setProp(property); +} + +/** + * Get a wallpaper manager. + * + * Checks for HydraPaper first and then for Superpaper. Falls back to the default manager. + * + * @returns {WallpaperManager} Wallpaper manager, falls back to the default manager + */ +// This function is here instead of wallpaperManager.js to work around looping import errors +function getWallpaperManager(): WallpaperManager { + const hydraPaper = new HydraPaper(); + if (hydraPaper.isAvailable()) + return hydraPaper; + + const superpaper = new Superpaper(); + if (superpaper.isAvailable()) + return superpaper; + + return new DefaultWallpaperManager(); +} + +/** + * Check if a filename matches a merged wallpaper name. + * + * Merged wallpaper need special handling as these are single images + * but span across all displays. + * + * @param {string} filename Naming to check + * @returns {boolean} Whether the image is a merged wallpaper + */ +// This function is here instead of wallpaperManager.js to work around looping import errors +function isImageMerged(filename: string): boolean { + return DefaultWallpaperManager.isImageMerged(filename) || + HydraPaper.isImageMerged(filename) || + Superpaper.isImageMerged(filename); +} + +export { + SourceType, + getSourceTypeName, + getWallpaperManager, + isImageMerged, + execCheck, + fileName, + findFirstDifference, + getMonitorCount, + getRandomNumber, + removeItemOnce, + setPictureUriOfSettingsObject, + shuffleArray +}; diff --git a/src/wallpaperController.ts b/src/wallpaperController.ts new file mode 100644 index 00000000..e3d610cf --- /dev/null +++ b/src/wallpaperController.ts @@ -0,0 +1,743 @@ +import Gio from 'gi://Gio'; +import GLib from 'gi://GLib'; + +// Legacy importing style for shell internal bindings not available in standard import format +const ExtensionUtils = imports.misc.extensionUtils; + +import * as HistoryModule from './history.js'; +import * as SettingsModule from './settings.js'; +import * as Utils from './utils.js'; + +import {AFTimer as Timer} from './timer.js'; +import {Logger} from './logger.js'; +import {Mode} from './manager/wallpaperManager.js'; + +// SourceAdapter +import {BaseAdapter} from './adapter/baseAdapter.js'; +import {GenericJsonAdapter} from './adapter/genericJson.js'; +import {LocalFolderAdapter} from './adapter/localFolder.js'; +import {RedditAdapter} from './adapter/reddit.js'; +import {UnsplashAdapter} from './adapter/unsplash.js'; +import {UrlSourceAdapter} from './adapter/urlSource.js'; +import {WallhavenAdapter} from './adapter/wallhaven.js'; + +const Self = ExtensionUtils.getCurrentExtension(); + +// https://gjs.guide/guides/gjs/asynchronous-programming.html#promisify-helper +Gio._promisify(Gio.File.prototype, 'move_async', 'move_finish'); + +interface RandomAdapterResult { + adapter: BaseAdapter, + id: string, + type: number, + imageCount: number + } + +/** + * The main wallpaper handler. + */ +class WallpaperController { + wallpaperLocation: string; + prohibitNewWallpaper = false; + + private _backendConnection = new SettingsModule.Settings(SettingsModule.RWG_SETTINGS_SCHEMA_BACKEND_CONNECTION); + private _logger = new Logger('RWG3', 'WallpaperController'); + private _settings = new SettingsModule.Settings(); + private _timer = Timer.getTimer(); + private _historyController: HistoryModule.HistoryController; + private _wallpaperManager = Utils.getWallpaperManager(); + private _autoFetch = {active: false, duration: 30}; + private _previewId: string | undefined; + private _resetWallpaper = false; + private _timeout: number | null = null; + /** functions will be called upon loading a new wallpaper */ + private _startLoadingHooks: (() => void)[] = []; + /** functions will be called when loading a new wallpaper stopped. */ + private _stopLoadingHooks: (() => void)[] = []; + private _observedValues: number[] = []; + private _observedBackgroundValues: number[] = []; + + /** + * Create a new controller. + * + * Should only exists once to avoid weird shenanigans because the extension background + * and preferences page existing in two different contexts. + */ + constructor() { + let xdg_cache_home = GLib.getenv('XDG_CACHE_HOME'); + if (!xdg_cache_home) { + const home = GLib.getenv('HOME'); + + if (home) + xdg_cache_home = `${home}/.cache`; + else + xdg_cache_home = '/tmp'; + } + + this.wallpaperLocation = `${xdg_cache_home}/${Self.metadata['uuid']}/wallpapers/`; + const mode = 0o0755; + GLib.mkdir_with_parents(this.wallpaperLocation, mode); + + this._historyController = new HistoryModule.HistoryController(this.wallpaperLocation); + + // Bring values to defined state + this._backendConnection.setBoolean('clear-history', false); + this._backendConnection.setBoolean('open-folder', false); + this._backendConnection.setBoolean('pause-timer', false); + this._backendConnection.setBoolean('request-new-wallpaper', false); + + // Track value changes + this._observedBackgroundValues.push(this._backendConnection.observe('clear-history', () => this._clearHistory())); + this._observedBackgroundValues.push(this._backendConnection.observe('open-folder', () => this._openFolder())); + this._observedBackgroundValues.push(this._backendConnection.observe('pause-timer', () => this._pauseTimer())); + this._observedBackgroundValues.push(this._backendConnection.observe('request-new-wallpaper', () => this._requestNewWallpaper().catch(error => { + this._logger.error(error); + }))); + + this._observedValues.push(this._settings.observe('history-length', () => this._updateHistory())); + this._observedValues.push(this._settings.observe('auto-fetch', () => this._updateAutoFetching())); + this._observedValues.push(this._settings.observe('minutes', () => this._updateAutoFetching())); + this._observedValues.push(this._settings.observe('hours', () => this._updateAutoFetching())); + + /** + * When the user installs a manager we won't notice that it's available. + * The preference window however checks on startup for availability and will allow this setting + * to change. Let's listen for that change and update our manager accordingly. + */ + this._observedValues.push(this._settings.observe('multiple-displays', () => this._updateWallpaperManager())); + + this._updateHistory(); + + // Fetching and merging wallpaper can be quite heavy on load so try doing this only when idle + GLib.idle_add(GLib.PRIORITY_DEFAULT_IDLE, () => { + // This may start the timer which might load a new wallpaper on interval surpassed + this._updateAutoFetching(); + + // load a new wallpaper on startup, but don't when the timer already fetched one because of a surpassed timer interval + if (this._settings.getBoolean('fetch-on-startup') && (!this._timer.isEnabled() || this._timer.minutesElapsed() > 1)) { + this.fetchNewWallpaper().catch(error => { + this._logger.error(error); + }); + } + + return GLib.SOURCE_REMOVE; + }); + + // Initialize favorites folder + // TODO: There's probably a better place for this + const favoritesFolderSetting = this._settings.getString('favorites-folder'); + let favoritesFolder: Gio.File; + if (favoritesFolderSetting === '') { + const directoryPictures = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES); + + if (directoryPictures === null) { + // Pictures not set up + const directoryDownloads = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_DOWNLOAD); + + if (directoryDownloads === null) { + const xdg_data_home = GLib.get_user_data_dir(); + favoritesFolder = Gio.File.new_for_path(xdg_data_home); + } else { + favoritesFolder = Gio.File.new_for_path(directoryDownloads); + } + } else { + favoritesFolder = Gio.File.new_for_path(directoryPictures); + } + + favoritesFolder = favoritesFolder.get_child(Self.metadata['uuid']); + + const favoritesFolderPath = favoritesFolder.get_path(); + if (favoritesFolderPath) + this._settings.setString('favorites-folder', favoritesFolderPath); + } + } + + /** + * Clean up extension remnants. + */ + cleanup(): void { + for (const observedValue of this._observedValues) + this._settings.disconnect(observedValue); + this._observedValues = []; + + for (const observedValue of this._observedBackgroundValues) + this._backendConnection.disconnect(observedValue); + this._observedBackgroundValues = []; + } + + /** + * Empty the history. (Background settings observer edition) + */ + private _clearHistory(): void { + if (this._backendConnection.getBoolean('clear-history')) { + this.update(); + this.deleteHistory(); + this._backendConnection.setBoolean('clear-history', false); + } + } + + /** + * Open the internal wallpaper cache folder. (Background settings observer edition) + */ + private _openFolder(): void { + if (this._backendConnection.getBoolean('open-folder')) { + const uri = GLib.filename_to_uri(this.wallpaperLocation, ''); + Gio.AppInfo.launch_default_for_uri(uri, Gio.AppLaunchContext.new()); + this._backendConnection.setBoolean('open-folder', false); + } + } + + /** + * Pause or resume the timer. (Background settings observer edition) + */ + private _pauseTimer(): void { + if (this._backendConnection.getBoolean('pause-timer')) { + this._timer.pause(); + } else { + this._timer.continue(); + + // Switching the switch in the menu closes the menu which triggers a hover event + // Prohibit that from emitting because a paused timer could have surpassed the interval + // and try to fetch new wallpaper which would be interrupted by a wallpaper reset caused + // by the closing menu event. + this.prohibitNewWallpaper = true; + + // And activate emitting again after a second + GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => { + this.prohibitNewWallpaper = false; + return GLib.SOURCE_REMOVE; + }); + } + } + + /** + * Get a fresh wallpaper. (Background settings observer edition) + */ + private async _requestNewWallpaper(): Promise { + if (this._backendConnection.getBoolean('request-new-wallpaper')) { + this.update(); + try { + await this.fetchNewWallpaper(); + } finally { + this.update(); + this._backendConnection.setBoolean('request-new-wallpaper', false); + } + } + } + + /** + * Update the history. + * + * Loads from settings. + */ + private _updateHistory(): void { + this._historyController.load(); + } + + /** + * Update settings related to the auto fetching. + */ + private _updateAutoFetching(): void { + let duration = 0; + duration += this._settings.getInt('minutes'); + duration += this._settings.getInt('hours') * 60; + this._autoFetch.duration = duration; + this._autoFetch.active = this._settings.getBoolean('auto-fetch'); + + if (this._autoFetch.active) { + this._timer.registerCallback(() => { + return this.fetchNewWallpaper(); + }); + this._timer.setMinutes(this._autoFetch.duration); + this._timer.start().catch(error => { + this._logger.error(error); + }); + } else { + this._timer.stop(); + } + } + + /** + * Update the wallpaper manager on settings change. + */ + private _updateWallpaperManager(): void { + this._wallpaperManager = Utils.getWallpaperManager(); + } + + /** + * Get an array of random adapter needed to fill the display $count. + * + * A single adapter can be assigned for multiple images so you may get less than $count adapter back. + * + * Returns a default UnsplashAdapter in case of failure. + * + * @param {number} count The amount of wallpaper requested + * @returns {RandomAdapterResult[]} Array of info objects how many images are needed for each adapter + */ + private _getRandomAdapter(count: number): RandomAdapterResult[] { + const sourceIDs = this._getRandomSource(count); + const randomAdapterResult: RandomAdapterResult[] = []; + + if (sourceIDs.length < 1 || sourceIDs[0] === '-1') { + randomAdapterResult.push({ + adapter: new UnsplashAdapter(null, null), + id: '-1', + type: 0, + imageCount: count, + }); + return randomAdapterResult; + } + + /** + * Check if we've chosen the same adapter type before. + * + * @param {RandomAdapterResult[]} array Array of already chosen adapter + * @param {number} type Type of the source + * @returns {RandomAdapterResult | null} Found adapter or null + */ + function _arrayIncludes(array: RandomAdapterResult[], type: number): RandomAdapterResult | null { + for (const element of array) { + if (element.type === type) + return element; + } + return null; + } + + for (let index = 0; index < sourceIDs.length; index++) { + const sourceID = sourceIDs[index]; + const path = `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${sourceID}/`; + const settingsGeneral = new SettingsModule.Settings(SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); + + let imageSourceAdapter: BaseAdapter; + let sourceName = 'undefined'; + let sourceType = -1; + + sourceName = settingsGeneral.getString('name'); + sourceType = settingsGeneral.getInt('type'); + + const availableAdapter = _arrayIncludes(randomAdapterResult, sourceType); + if (availableAdapter) { + availableAdapter.imageCount++; + continue; + } + + try { + switch (sourceType) { + case Utils.SourceType.UNSPLASH: + imageSourceAdapter = new UnsplashAdapter(sourceID, sourceName); + break; + case Utils.SourceType.WALLHAVEN: + imageSourceAdapter = new WallhavenAdapter(sourceID, sourceName); + break; + case Utils.SourceType.REDDIT: + imageSourceAdapter = new RedditAdapter(sourceID, sourceName); + break; + case Utils.SourceType.GENERIC_JSON: + imageSourceAdapter = new GenericJsonAdapter(sourceID, sourceName); + break; + case Utils.SourceType.LOCAL_FOLDER: + imageSourceAdapter = new LocalFolderAdapter(sourceID, sourceName); + break; + case Utils.SourceType.STATIC_URL: + imageSourceAdapter = new UrlSourceAdapter(sourceID, sourceName); + break; + default: + imageSourceAdapter = new UnsplashAdapter(null, null); + sourceType = 0; + break; + } + } catch (error) { + this._logger.warn('Had errors, fetching with default settings.'); + imageSourceAdapter = new UnsplashAdapter(null, null); + sourceType = Utils.SourceType.UNSPLASH; + } + + randomAdapterResult.push({ + adapter: imageSourceAdapter, + id: sourceID, + type: sourceType, + imageCount: 1, + }); + } + + return randomAdapterResult; + } + + /** + * Gets randomly $count amount of enabled sources. + * + * The same source can appear multiple times in the resulting array. + * + * @param {number} count Amount of requested source IDs + * @returns {string[]} Array of source IDs or ['-1'] in case of failure + */ + private _getRandomSource(count: number): string[] { + const sourceResult: string[] = []; + const sources: string[] = this._settings.getStrv('sources'); + + if (sources === null || sources.length < 1) + return ['-1']; + + const enabledSources = sources.filter(element => { + const path = `${SettingsModule.RWG_SETTINGS_SCHEMA_PATH}/sources/general/${element}/`; + const settingsGeneral = new SettingsModule.Settings(SettingsModule.RWG_SETTINGS_SCHEMA_SOURCES_GENERAL, path); + return settingsGeneral.getBoolean('enabled'); + }); + + if (enabledSources === null || enabledSources.length < 1) + return ['-1']; + + for (let index = 0; index < count; index++) { + const chosenSource = enabledSources[Utils.getRandomNumber(enabledSources.length)]; + sourceResult.push(chosenSource); + } + + return sourceResult; + } + + /** + * Run a configured post command. + */ + private _runPostCommands(): void { + const backgroundSettings = new SettingsModule.Settings('org.gnome.desktop.background'); + const commandString = this._settings.getString('general-post-command'); + + // Read the current wallpaper uri from settings because it could be a merged wallpaper + // Remove prefix "file://" to get the real path + const currentWallpaperPath = backgroundSettings.getString('picture-uri').replace(/^file:\/\//, ''); + + // TODO: this ignores the lock-screen + const generalPostCommandArray = this._getCommandArray(commandString, currentWallpaperPath); + if (generalPostCommandArray !== null) { + // Do not await this call, let it be one shot + Utils.execCheck(generalPostCommandArray).catch(error => { + this._logger.error(error); + }); + } + } + + /** + * Fill an array with images from the history until $count. + * + * @param {string[]} wallpaperArray Array of wallpaper paths + * @param {number | undefined} requestCount Amount of wallpaper paths $wallpaperArray should contain, defaults to the value reported by _getCurrentDisplayCount() + * @returns {string[]} Array of wallpaper paths matching the length of $count + */ + private _fillDisplaysFromHistory(wallpaperArray: string[], requestCount?: number): string[] { + const count = requestCount ?? this._getCurrentDisplayCount(); + const newWallpaperArray: string[] = [...wallpaperArray]; + + // Abuse history to fill missing images + for (let index = newWallpaperArray.length; index < count; index++) { + let historyElement: HistoryModule.HistoryEntry; + do + historyElement = this._historyController.getRandom(); + while (this._historyController.history.length > count && historyElement.path && newWallpaperArray.includes(historyElement.path)); + // try to ensure different wallpaper for all displays if possible + + if (historyElement.path) + newWallpaperArray.push(historyElement.path); + } + + // Trim array if we have too many images, possibly by having a too long input array + return newWallpaperArray.slice(0, count); + } + + /** + * Set an existing history entry as wallpaper. + * + * @param {string} historyId Unique ID + */ + async setWallpaper(historyId: string): Promise { + const historyElement = this._historyController.get(historyId); + + if (historyElement?.id && historyElement.path && this._historyController.promoteToActive(historyElement.id)) { + const changeType = this._settings.getInt('change-type') as Mode; + const usedWallpaperPaths = this._fillDisplaysFromHistory([historyElement.path]); + + // ignore changeType === Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT because that doesn't make sense + // when requesting a specific history entry + if (changeType > Mode.BACKGROUND_AND_LOCKSCREEN) + await this._wallpaperManager.setWallpaper(usedWallpaperPaths, Mode.BACKGROUND_AND_LOCKSCREEN); + else + await this._wallpaperManager.setWallpaper(usedWallpaperPaths, changeType); + + this._runPostCommands(); + usedWallpaperPaths.reverse().forEach(path => { + const id = this._historyController.getEntryByPath(path)?.id; + if (id) + this._historyController.promoteToActive(id); + }); + } else { + this._logger.warn(`The history id (${historyId}) could not be found.`); + } + // TODO: Error handling history id not found. + } + + /** + * Fetch fresh wallpaper. + */ + async fetchNewWallpaper(): Promise { + this._startLoadingHooks.forEach(element => element()); + + try { + const changeType = this._settings.getInt('change-type') as Mode; + let monitorCount = this._getCurrentDisplayCount(); + + // Request double the amount of displays if we need background and lock screen + if (changeType === Mode.BACKGROUND_AND_LOCKSCREEN_INDEPENDENT) + monitorCount *= 2; + + const imageAdapters = this._getRandomAdapter(monitorCount); + + const randomImagePromises = imageAdapters.map(element => { + return element.adapter.requestRandomImage(element.imageCount); + }); + const newWallpapers = await Promise.allSettled(randomImagePromises); + + const fetchPromises = newWallpapers.flatMap((object, index) => { + const fetchPromiseArray: Promise[] = []; + let array: HistoryModule.HistoryEntry[] = []; + + // rejected promises + if ('reason' in object && Array.isArray(object.reason) && object.reason.length > 0 && object.reason[0] instanceof HistoryModule.HistoryEntry) + array = object.reason as HistoryModule.HistoryEntry[]; + + // fulfilled promises + if ('value' in object) + array = object.value; + + for (const element of array) { + element.adapter = { + id: imageAdapters[index].id, + type: imageAdapters[index].type, + }; + + this._logger.debug(`Requesting image: ${element.source.imageDownloadUrl}`); + fetchPromiseArray.push(imageAdapters[index].adapter.fetchFile(element)); + } + + return fetchPromiseArray; + }); + + if (fetchPromises.length < 1) + throw new Error('Unable to request new images.'); + + // wait for all fetching images + this._logger.info(`Requesting ${fetchPromises.length} new images.`); + const newImageEntriesPromiseResults = await Promise.allSettled(fetchPromises); + + const newImageEntries = newImageEntriesPromiseResults.map(element => { + if (element.status !== 'fulfilled' && !('value' in element)) + return null; + + return element.value; + }).filter(element => { + return element instanceof HistoryModule.HistoryEntry; + }) as HistoryModule.HistoryEntry[]; + + this._logger.debug(`Fetched ${newImageEntries.length} new images.`); + const newWallpaperPaths = newImageEntries.map(element => { + return element.path; + }); + + if (newWallpaperPaths.length < 1) + throw new Error('Unable to fetch new images.'); + + if (newWallpaperPaths.length < monitorCount) + this._logger.warn('Unable to fill all displays with new images.'); + + const usedWallpaperPaths = this._fillDisplaysFromHistory(newWallpaperPaths, monitorCount); + + await this._wallpaperManager.setWallpaper(usedWallpaperPaths, changeType); + + usedWallpaperPaths.reverse().forEach(path => { + const id = this._historyController.getEntryByPath(path)?.id; + if (id) + this._historyController.promoteToActive(id); + }); + + // insert new wallpapers into history + this._historyController.insert(newImageEntries.reverse()); + + this._runPostCommands(); + } catch (error) { + this._logger.error(error); + } finally { + this._stopLoadingHooks.forEach(element => element()); + } + } + + // TODO: Change to original historyElement if more variable get exposed + /** + * Get a command array from a string. + * + * Fills variables if found: + * - %wallpaper_path% + * + * @param {string} commandString String to parse an array from + * @param {string} historyElementPath Wallpaper path to fill into the variable + * @returns {string[] | null} Command array or null + */ + private _getCommandArray(commandString: string, historyElementPath: string): string[] | null { + let string = commandString; + if (string === '') + return null; + + // Replace variables + const variables = new Map(); + variables.set('%wallpaper_path%', historyElementPath); + + variables.forEach((value, key) => { + string = string.replaceAll(key, value); + }); + + try { + // https://gjs-docs.gnome.org/glib20/glib.shell_parse_argv + // Parses a command line into an argument vector, in much the same way + // the shell would, but without many of the expansions the shell would + // perform (variable expansion, globs, operators, filename expansion, + // etc. are not supported). + return GLib.shell_parse_argv(string)[1]; + } catch (e) { + this._logger.warn(String(e)); + } + + return null; + } + + /** + * Get the current number of displays. + * + * This also takes the user setting and wallpaper manager availability into account + * and lies accordingly by reporting only 1 display. + * + * @returns {number} Amount of currently activated displays or 1 + */ + private _getCurrentDisplayCount(): number { + if (!this._settings.getBoolean('multiple-displays')) + return 1; + + if (!this._wallpaperManager.canHandleMultipleImages) + return 1; + + return Utils.getMonitorCount(); + } + + /** + * Set a background after a $delay + * + * Prohibits quick wallpaper changing by blocking additional change requests + * within a timeout. + * + * @param {string[] | undefined} paths Array of wallpaper paths + * @param {number | undefined} delay Delay, defaults to 200ms + */ + private _backgroundTimeout(paths?: string[], delay?: number): void { + if (this._timeout || !paths) + return; + + delay = delay || 200; + + this._timeout = GLib.timeout_add(GLib.PRIORITY_DEFAULT, delay, () => { + this._timeout = null; + + // Only change the background - the lock screen wouldn't be visible anyway + // because this function is only used for hover preview + if (this._resetWallpaper) { + this._wallpaperManager.setWallpaper(paths).catch(error => { + this._logger.error(error); + }); + this._resetWallpaper = false; + } else if (this._previewId !== undefined) { + this._wallpaperManager.setWallpaper(paths).catch(error => { + this._logger.error(error); + }); + } + + return GLib.SOURCE_REMOVE; + }); + } + + /** + * Preview an image in the history. + * + * @param {string} historyId Unique ID + * @param {number} delay Delay, defaults to 200ms + */ + previewWallpaper(historyId: string, delay?: number): void { + if (!this._settings.getBoolean('disable-hover-preview')) { + this._previewId = historyId; + this._resetWallpaper = false; + + // Do not fill other displays here. + // Merging images can take a long time and hurt the quick preview purpose. + // Therefor only an array with a single wallpaper path here: + const newWallpaperPaths = [this.wallpaperLocation + this._previewId]; + + this._backgroundTimeout(newWallpaperPaths, delay); + } + } + + /** + * Set the wallpaper to an URI. + * + * @param {string} uri Wallpaper URI + */ + resetWallpaper(uri: string): void { + if (!this._settings.getBoolean('disable-hover-preview')) { + this._resetWallpaper = true; + // FIXME: With an already running timeout this reset request will be ignored + this._backgroundTimeout([GLib.filename_from_uri(uri)[0]]); + } + } + + /** + * Get the HistoryController. + * + * @returns {HistoryModule.HistoryController} The history controller + */ + getHistoryController(): HistoryModule.HistoryController { + return this._historyController; + } + + /** + * Empty the history. + */ + deleteHistory(): void { + this._historyController.clear(); + } + + /** + * Update the history. + */ + update(): void { + this._updateHistory(); + } + + /** + * Register a function which gets called on wallpaper fetching. + * + * Can take multiple hooks. + * + * @param {() => void} fn Function to call + */ + registerStartLoadingHook(fn: () => void): void { + if (typeof fn === 'function') + this._startLoadingHooks.push(fn); + } + + /** + * Register a function which gets called when done wallpaper fetching. + * + * Can take multiple hooks. + * + * @param {() => void} fn Function to call + */ + registerStopLoadingHook(fn: () => void): void { + if (typeof fn === 'function') + this._stopLoadingHooks.push(fn); + } +} + +export {WallpaperController}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..8f3bab76 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,45 @@ +{ + "compilerOptions": { + "experimentalDecorators": true, + "target": "ES2020", + "module": "es2020", + "sourceMap": false, + "strict": true, + "pretty": true, + "removeComments": false, + "baseUrl": "./src", + "allowSyntheticDefaultImports": true, + "outDir": "./randomwallpaper@iflow.space", + "moduleResolution": "node", + "skipLibCheck": true, + "lib": [ + "ES2021", + "DOM", // FIXME: This is here for TextDecoder which should be defined by global GJS + // https://gjs-docs.gnome.org/gjs/encoding.md + // > The functions in this module are available globally, without import. + ], + "typeRoots": [ + "./node_modules/@gi-types", + "./types", + ], + "types": [ + "gjs-environment", + "base-types", + "gtk4-types", + "adw1", + "gtk4/adw", // locally expose 'gi://Adw' and extend with EntryRow + "mappings", // missing mappings 'from @gi-types' to 'gi://' + "ui", // shell gi imports in legacy import style + "misc", // shell gi imports in legacy import style + ], + + }, + "include": [ + "src/**/*.ts", + "randomwallpaper@iflow.space/**/*.js" + ], + "exclude": [ + "./node_modules/**", + "./types/**" + ], +} diff --git a/types/gtk4/adw/index.d.ts b/types/gtk4/adw/index.d.ts new file mode 100644 index 00000000..39c6e8ee --- /dev/null +++ b/types/gtk4/adw/index.d.ts @@ -0,0 +1,106 @@ +// The mapping from @gi-types/adw1 to gi://Adw is somehow missing but exists in the real gnome environment +declare module 'gi://Adw' { + export * from '@gi-types/adw1'; + + // https://github.com/gi-ts/gtk4/tree/master/packages/%40gi-types/adw1 + // Manually extend Adw.EntryRow which is somehow missing from @gi-types/adw1 + // TODO: Remove this file and links to it once the original source updates to a recent Adw version. + import Adw from 'gi://Adw'; + import Gtk from 'gi://Gtk'; + import GObject from 'gi://GObject'; + + export module EntryRow { + export interface ConstructorProperties extends Adw.PreferencesRow.ConstructorProperties { + [key: string]: any; + activates_default: boolean; + attributes: unknown; + enable_emoji_completion: boolean; + input_hints: unknown; + input_purpose: unknown; + show_apply_button: boolean; + test: string; + } + } + + export class EntryRow extends Adw.PreferencesRow implements Gtk.Accessible, Gtk.Actionable, Gtk.Buildable, Gtk.Editable, Gtk.ConstraintTarget { + static $gtype: GObject.GType; + + constructor(properties?: Partial, ...args: any[]); + _init(properties?: Partial, ...args: any[]): void; + + // Properties + get activates_default(): boolean; + set activates_default(val: boolean); + get attributes(): unknown; + set attributes(val: unknown); + get enable_emoji_completion(): boolean; + set enable_emoji_completion(val: boolean); + get input_hints(): unknown; + set input_hints(val: unknown); + get input_purpose(): unknown; + set input_purpose(val: unknown); + get show_apply_button(): boolean; + set show_apply_button(val: boolean); + get text(): string; + set text(val: string); + + static ['new'](): EntryRow; + + add_prefix(val: Gtk.Widget): void; + add_suffix(val: Gtk.Widget): void; + remove(val: Gtk.Widget): void; + + // autogenerated: + + cursor_position: number; + cursorPosition: number; + editable: boolean; + enable_undo: boolean; + enableUndo: boolean; + max_width_chars: number; + maxWidthChars: number; + selection_bound: number; + selectionBound: number; + width_chars: number; + widthChars: number; + xalign: number; + delete_selection(): void; + delete_text(start_pos: number, end_pos: number): void; + finish_delegate(): void; + get_alignment(): number; + get_chars(start_pos: number, end_pos: number): string; + get_delegate(): Gtk.EditablePrototype | null; + get_editable(): boolean; + get_enable_undo(): boolean; + get_max_width_chars(): number; + get_position(): number; + get_selection_bounds(): [boolean, number, number]; + get_text(): string; + get_width_chars(): number; + init_delegate(): void; + insert_text(text: string, length: number, position: number): number; + select_region(start_pos: number, end_pos: number): void; + set_alignment(xalign: number): void; + set_editable(is_editable: boolean): void; + set_enable_undo(enable_undo: boolean): void; + set_max_width_chars(n_chars: number): void; + set_position(position: number): void; + set_text(text: string): void; + set_width_chars(n_chars: number): void; + vfunc_changed(): void; + vfunc_delete_text(start_pos: number, end_pos: number): void; + vfunc_do_delete_text(start_pos: number, end_pos: number): void; + vfunc_do_insert_text(text: string, length: number, position: number): number; + vfunc_get_delegate(): Gtk.EditablePrototype | null; + vfunc_get_selection_bounds(): [boolean, number, number]; + vfunc_get_text(): string; + vfunc_insert_text(text: string, length: number, position: number): number; + vfunc_set_selection_bounds(start_pos: number, end_pos: number): void; + } +} + +// extend gi imports interface with adw +// https://github.com/gi-ts/environment#importsgi +declare interface GjsGiImports { + Adw: typeof import('gi://Adw'); +} diff --git a/types/mappings/index.d.ts b/types/mappings/index.d.ts new file mode 100644 index 00000000..271e65af --- /dev/null +++ b/types/mappings/index.d.ts @@ -0,0 +1,16 @@ +// These mappings from '@gi-types/*' to 'gi://*' are somehow missing but exist in the real gnome environment +declare module 'gi://Clutter' { + export * from '@gi-types/clutter'; +} + +declare module 'gi://Cogl' { + export * from '@gi-types/cogl'; +} + +declare module 'gi://Meta' { + export * from '@gi-types/meta'; +} + +declare module 'gi://St' { + export * from '@gi-types/st'; +} diff --git a/types/misc/index.d.ts b/types/misc/index.d.ts new file mode 100644 index 00000000..e9a4589d --- /dev/null +++ b/types/misc/index.d.ts @@ -0,0 +1,147 @@ +/* eslint-disable */ + +declare module 'extensionUtils' { + // https://github.com/yilozt/rounded-window-corners/blob/main/%40imports/misc/extensionUtils.d.ts + // GPL3 + + import Gio from 'gi://Gio'; + + /** + * getCurrentExtension: + * + * @returns {?object} - The current extension, or null if not called from + * an extension. + */ + export function getCurrentExtension(): { + uuid: string, + path: string, + dir: Gio.File, + metadata: { + 'settings-schema': string, + uuid: string, + } + }; + /** + * initTranslations: + * @param {string=} domain - the gettext domain to use + * + * Initialize Gettext to load translations from extensionsdir/locale. + * If @domain is not provided, it will be taken from metadata['gettext-domain'] + */ + export function initTranslations(domain?: string | undefined): void; + /** + * gettext: + * @param {string} str - the string to translate + * + * Translate @str using the extension's gettext domain + * + * @returns {string} - the translated string + * + */ + export function gettext(str: string): string; + /** + * ngettext: + * @param {string} str - the string to translate + * @param {string} strPlural - the plural form of the string + * @param {number} n - the quantity for which translation is needed + * + * Translate @str and choose plural form using the extension's + * gettext domain + * + * @returns {string} - the translated string + * + */ + export function ngettext(str: string, strPlural: string, n: number): string; + /** + * pgettext: + * @param {string} context - context to disambiguate @str + * @param {string} str - the string to translate + * + * Translate @str in the context of @context using the extension's + * gettext domain + * + * @returns {string} - the translated string + * + */ + export function pgettext(context: string, str: string): string; + export function callExtensionGettextFunc(func: any, ...args: any[]): any; + /** + * getSettings: + * @param {string?} schema - the GSettings schema id + * @returns {Gio.Settings} - a new settings object for @schema + * + * Builds and returns a GSettings schema for @schema, using schema files + * in extensionsdir/schemas. If @schema is omitted, it is taken from + * metadata['settings-schema']. + */ + export function getSettings(schema?: string | undefined): Gio.Settings; + /** + * openPrefs: + * + * Open the preference dialog of the current extension + */ + export function openPrefs(): Promise; + export function isOutOfDate(extension: any): boolean; + export function serializeExtension(extension: any): {}; + export function deserializeExtension(variant: any): { + metadata: {}; + }; + export function installImporter(extension: any): void; + export const Gettext: any; + export const Config: any; + export namespace ExtensionType { + const SYSTEM: number; + const PER_USER: number; + } + export namespace ExtensionState { + const ENABLED: number; + const DISABLED: number; + const ERROR: number; + const OUT_OF_DATE: number; + const DOWNLOADING: number; + const INITIALIZED: number; + const UNINSTALLED: number; + } + export const SERIALIZED_PROPERTIES: string[]; +} + +declare interface GjsMiscImports { + extensionUtils: typeof import('extensionUtils'); +} + +// extend imports interface with misc elements +declare interface GjsImports { + misc: GjsMiscImports; +} + +declare module 'ExtensionMeta' { + import type {File} from 'gi://Gio'; + + /** + * An object describing the extension and various properties available for extensions to use. + * + * Some properties may only be available in some versions of GNOME Shell, while others may not be meant for extension authors to use. All properties should be considered read-only. + */ + export class ExtensionMeta { + /** the metadata.json file, parsed as JSON */ + readonly metadata: unknown; + /** the extension UUID */ + readonly uuid: string; + /** the extension type; `1` for system, `2` for user */ + readonly type: number; + /** the extension directory */ + readonly dir: File; + /** the extension directory path */ + readonly path: string; + /** an error message or an empty string if no error */ + readonly error: string; + /** whether the extension has a preferences dialog */ + readonly hasPrefs: boolean; + /** whether the extension has a pending update */ + readonly hasUpdate: boolean; + /** whether the extension can be enabled/disabled */ + readonly canChange: boolean; + /** a list of supported session modes */ + readonly sessionModes: string[]; + } +} diff --git a/types/ui/index.d.ts b/types/ui/index.d.ts new file mode 100644 index 00000000..3ad783a4 --- /dev/null +++ b/types/ui/index.d.ts @@ -0,0 +1,17 @@ +/* eslint-disable */ +// doing similar to https://github.com/gi-ts/environment + +/// +/// +/// + +declare interface GjsUiImports { + main: typeof import('main'); + panelMenu: typeof import('panelMenu'); + popupMenu: typeof import('popupMenu'); +} + +// extend imports interface with ui elements +declare interface GjsImports { + ui: GjsUiImports; +} diff --git a/types/ui/main.d.ts b/types/ui/main.d.ts new file mode 100644 index 00000000..d0c00bfd --- /dev/null +++ b/types/ui/main.d.ts @@ -0,0 +1,13 @@ +/* eslint-disable */ + +declare module 'main' { + import St from 'gi://St'; + + import * as PanelMenu from 'panelMenu'; + + export class Panel extends St.Widget { + addToStatusArea(role: string, indicator: PanelMenu.Button, position?: number, box?: unknown): PanelMenu.Button + } + + export const panel: Panel; +} diff --git a/types/ui/panelMenu.d.ts b/types/ui/panelMenu.d.ts new file mode 100644 index 00000000..a22857b9 --- /dev/null +++ b/types/ui/panelMenu.d.ts @@ -0,0 +1,15 @@ +/* eslint-disable */ + +declare module 'panelMenu' { + import St from 'gi://St'; + + import {PopupMenu} from 'popupMenu'; + + export class ButtonBox extends St.Widget{} + + export class Button extends ButtonBox { + menu: PopupMenu; + + constructor(menuAlignment: number, nameText: string, dontCreateMenu?: boolean); + } +} diff --git a/types/ui/popupMenu.d.ts b/types/ui/popupMenu.d.ts new file mode 100644 index 00000000..69f52806 --- /dev/null +++ b/types/ui/popupMenu.d.ts @@ -0,0 +1,75 @@ +/* eslint-disable */ + +declare module 'popupMenu' { + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/ui/popupMenu.js + + import Clutter from 'gi://Clutter'; + import St from 'gi://St'; + + // https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/js/misc/signals.js + export class EventEmitter { + connectObject(args: unknown): unknown; + connect_object(args: unknown): unknown; + disconnect_object(args: unknown): unknown; + disconnectObject(args: unknown): unknown; + + // don't know where these are: + connect(key: string, callback: (actor: typeof this, ...args: unknown[]) => unknown): void; + } + + export class PopupMenuBase extends EventEmitter { + actor: Clutter.Actor; + box: St.BoxLayout; + + addMenuItem(menuItem: PopupMenuSection | PopupSubMenuMenuItem | PopupSeparatorMenuItem | PopupBaseMenuItem, position?: number): void; + removeAll(): void; + } + + export class PopupBaseMenuItem extends St.BoxLayout { + actor: typeof this; + // get actor(): typeof this; + get sensitive(): boolean; + set sensitive(sensitive: boolean); + } + + export class PopupMenu extends PopupMenuBase { + constructor(sourceActor: Clutter.Actor, arrowAlignment: unknown, arrowSide: unknown) + } + + export class PopupMenuItem extends PopupBaseMenuItem { + constructor(text: string, params?: unknown) + label: St.Label; + } + + export class PopupSubMenuMenuItem extends PopupBaseMenuItem { + constructor(text: string, wantIcon: boolean) + + label: St.Label; + menu: PopupSubMenu; + } + + export class PopupSubMenu extends PopupMenuBase { + actor: St.ScrollView; + } + + export class PopupMenuSection extends PopupMenuBase { + actor: St.BoxLayout | Clutter.Actor; + } + + export class PopupSeparatorMenuItem extends PopupBaseMenuItem {} + export class Switch extends St.Bin {} + export class PopupSwitchMenuItem extends PopupBaseMenuItem { + constructor(text: string, active: boolean, params?: { + reactive: boolean | undefined, + activate: boolean | undefined, + hover: boolean | undefined, + style_class: unknown | null | undefined, + can_focus: boolean | undefined + }) + + setToggleState(state: boolean): void; + } + export class PopupImageMenuItem extends PopupBaseMenuItem {} + export class PopupDummyMenu extends EventEmitter {} + export class PopupMenuManager {} +}