Add wallpaper switch
This commit is contained in:
@@ -30,11 +30,53 @@ let
|
||||
|
||||
availableVariants = concatStringsSep " " (attrNames themeVariants);
|
||||
|
||||
# `variant:schemeFile` pairs so the wallpaper picker can read each variant's
|
||||
# base16 palette at runtime.
|
||||
variantSchemes = concatStringsSep " "
|
||||
(mapAttrsToList (variant: v: "${variant}:${v.scheme}") themeVariants);
|
||||
|
||||
# variant:doomtheme pairs consumed by the theme-switch script's
|
||||
# best-effort Doom Emacs hook (see ./theme-switch.sh).
|
||||
doomThemeMap = concatStringsSep " "
|
||||
(mapAttrsToList (variant: theme: "${variant}:${theme}") cfg.doomThemes);
|
||||
|
||||
# Stylix is only declared on hosts that import its Home Manager module
|
||||
# (e.g. `fujin`). Servers (susano, izanagi, amaterasu, ...) don't, so
|
||||
# `stylix` isn't declared there. We probe option *declarations* (not
|
||||
# config values) to avoid an evaluation cycle.
|
||||
hasStylix = options ? stylix;
|
||||
|
||||
# --- Wallpaper handling (runtime-cloned repo + palette matching) ----------
|
||||
wallpapersEnabled = cfg.wallpaperRepo != null;
|
||||
wallpapersDir = "${config.home.homeDirectory}/Wallpapers";
|
||||
# hyprpaper's config points here; set-wallpaper rebinds this symlink, so we
|
||||
# never have to write hyprpaper config ourselves.
|
||||
wallpaperSymlink = "${config.xdg.stateHome}/theme/wallpaper-current";
|
||||
|
||||
wallpaper-pick = pkgs.writeShellScriptBin "wallpaper-pick" ''
|
||||
SCRIPT_TAG="wallpaper-pick"
|
||||
${builtins.readFile ./log.sh}
|
||||
wallpapersDir="${wallpapersDir}"
|
||||
wallpaperRepo="${cfg.wallpaperRepo or ""}"
|
||||
variantSchemes="${variantSchemes}"
|
||||
MAGICK="${getExe pkgs.imagemagick}"
|
||||
GIT="${getExe pkgs.git}"
|
||||
${builtins.readFile ./wallpaper-pick.sh}
|
||||
'';
|
||||
|
||||
wallpaper-set = pkgs.writeShellScriptBin "wallpaper-set" ''
|
||||
SCRIPT_TAG="wallpaper-set"
|
||||
${builtins.readFile ./log.sh}
|
||||
wallpaperSymlink="${wallpaperSymlink}"
|
||||
${builtins.readFile ./set-wallpaper.sh}
|
||||
'';
|
||||
|
||||
# Applied once at login (after hyprpaper) to seed a base-variant wallpaper.
|
||||
wallpaper-init = pkgs.writeShellScript "wallpaper-init" ''
|
||||
wp="$(${getExe wallpaper-pick} ${baseVariant} 2>/dev/null)" || exit 0
|
||||
[ -n "$wp" ] && ${getExe wallpaper-set} "$wp" || true
|
||||
'';
|
||||
|
||||
# Userspace theme switcher: activates a pre-built Home Manager
|
||||
# specialisation generation. No sudo, no OS rebuild — just runs the
|
||||
# specialisation's activation script in the base HM generation.
|
||||
@@ -42,17 +84,16 @@ let
|
||||
# The script logic lives in ./theme-switch.sh (plain shell, no Nix
|
||||
# escaping); we only inject the config-derived values it needs.
|
||||
theme-switch = pkgs.writeShellScriptBin "theme-switch" ''
|
||||
SCRIPT_TAG="theme-switch"
|
||||
${builtins.readFile ./log.sh}
|
||||
baseVariant="${baseVariant}"
|
||||
availableVariants="${availableVariants}"
|
||||
doomThemes="${doomThemeMap}"
|
||||
wallpapersEnabled="${if wallpapersEnabled then "1" else "0"}"
|
||||
wallpaperPickBin="${if wallpapersEnabled then getExe wallpaper-pick else ""}"
|
||||
wallpaperSetBin="${if wallpapersEnabled then getExe wallpaper-set else ""}"
|
||||
${builtins.readFile ./theme-switch.sh}
|
||||
'';
|
||||
|
||||
# Stylix is only declared on hosts that import its Home Manager module
|
||||
# (e.g. `fujin`). Servers (susano, izanagi, amaterasu, ...) don't, so
|
||||
# `stylix` isn't declared there. We probe option *declarations* (not
|
||||
# config values) to avoid an evaluation cycle.
|
||||
hasStylix = options ? stylix;
|
||||
in {
|
||||
options.dov.dynamic-theme = {
|
||||
enable = mkEnableOption "dynamic theme switching";
|
||||
@@ -95,24 +136,43 @@ in {
|
||||
map the variants to `catppuccin-mocha` / `catppuccin-latte`.
|
||||
'';
|
||||
};
|
||||
|
||||
wallpaperRepo = mkOption {
|
||||
type = types.nullOr types.str;
|
||||
default = null;
|
||||
example = "https://example.org/me/wallpapers.git";
|
||||
description = ''
|
||||
Git URL of a wallpaper repository. When set, theme-switch also picks
|
||||
a random wallpaper whose palette matches the current variant and
|
||||
applies it via hyprpaper. The repo is cloned to
|
||||
`~/Wallpapers` on first use (a one-time, runtime cost — it is NOT
|
||||
fetched at build time, so `nix build` stays fast).
|
||||
'';
|
||||
};
|
||||
};
|
||||
|
||||
config = mkIf cfg.enable (
|
||||
config = mkIf cfg.enable (mkMerge [
|
||||
{
|
||||
home.packages = [ theme-switch ];
|
||||
home.packages = [ theme-switch ]
|
||||
++ optionals wallpapersEnabled [ wallpaper-pick wallpaper-set ];
|
||||
}
|
||||
|
||||
# Only reference the `stylix` option where it is actually declared.
|
||||
# `mkIf false` is NOT enough: the module system still rejects a
|
||||
# definition (even a disabled one) for an option that does not exist,
|
||||
# which breaks every host that doesn't import stylix. `optionalAttrs`
|
||||
# removes the key structurally, so non-stylix hosts evaluate cleanly.
|
||||
// optionalAttrs hasStylix {
|
||||
# (keyed on `options ?`, i.e. *declarations* — never a config value) is
|
||||
# the recursion-free way to gate structure on stylix presence.
|
||||
(optionalAttrs hasStylix {
|
||||
stylix = {
|
||||
enable = true;
|
||||
autoEnable = true;
|
||||
# Base scheme/polarity; overridable per specialisation below.
|
||||
base16Scheme = mkDefault themeVariants.${baseVariant}.scheme;
|
||||
polarity = mkDefault themeVariants.${baseVariant}.polarity;
|
||||
# When we manage hyprpaper's wallpaper ourselves, disable stylix's
|
||||
# hyprpaper target so the two don't fight over the wallpaper path.
|
||||
targets.hyprpaper.enable = mkForce (!wallpapersEnabled);
|
||||
};
|
||||
|
||||
# Generate Home Manager specialisations for every non-base variant.
|
||||
@@ -126,6 +186,32 @@ in {
|
||||
};
|
||||
};
|
||||
}) specialisationVariants;
|
||||
})
|
||||
|
||||
# Wallpaper handling. These options always exist structurally (HM ships
|
||||
# services.hyprpaper and a freeform systemd.user.services), so we gate
|
||||
# the *values* with mkIf/optionals rather than shaping structure — that
|
||||
# avoids the config-depends-on-config recursion that optionalAttrs on a
|
||||
# config value would cause.
|
||||
{
|
||||
services.hyprpaper.enable = wallpapersEnabled;
|
||||
services.hyprpaper.settings = mkIf wallpapersEnabled {
|
||||
splash = false;
|
||||
wallpaper = [{ monitor = ""; path = wallpaperSymlink; }];
|
||||
};
|
||||
|
||||
systemd.user.services.wallpaper-init = mkIf wallpapersEnabled {
|
||||
Unit = {
|
||||
Description = "Pick and apply a wallpaper matching the base theme";
|
||||
After = [ "hyprpaper.service" ];
|
||||
PartOf = [ "graphical-session.target" ];
|
||||
};
|
||||
Service = {
|
||||
Type = "oneshot";
|
||||
ExecStart = "${wallpaper-init}";
|
||||
};
|
||||
Install.WantedBy = [ "graphical-session.target" ];
|
||||
};
|
||||
}
|
||||
);
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
# shellcheck shell=bash
|
||||
# Colorful, TTY-aware logging helpers: log_info / log_warn / log_error.
|
||||
#
|
||||
# Everything is written to *stderr*, so a script's stdout stays a clean data
|
||||
# channel (e.g. wallpaper-pick prints the chosen image path on stdout while
|
||||
# its progress goes to stderr). Set SCRIPT_TAG to prefix every line with the
|
||||
# originating script, e.g. "[theme-switch] selecting wallpaper...".
|
||||
#
|
||||
# Colors are auto-disabled when stderr isn't a terminal or NO_COLOR is set.
|
||||
__theme_log_init() {
|
||||
if [ -t 2 ] && [ -z "${NO_COLOR:-}" ]; then
|
||||
__C_RESET=$'\033[0m'
|
||||
__C_INFO=$'\033[1;34m' # blue
|
||||
__C_WARN=$'\033[1;33m' # yellow
|
||||
__C_ERR=$'\033[1;31m' # red
|
||||
else
|
||||
__C_RESET=""; __C_INFO=""; __C_WARN=""; __C_ERR=""
|
||||
fi
|
||||
}
|
||||
__theme_log_init
|
||||
|
||||
log_info() { printf '%s%s%s%s\n' "${__C_INFO}" "${SCRIPT_TAG:+[$SCRIPT_TAG] }" "$*" "${__C_RESET}" >&2; }
|
||||
log_warn() { printf '%s%s%s%s\n' "${__C_WARN}" "${SCRIPT_TAG:+[$SCRIPT_TAG] }" "warning: $*" "${__C_RESET}" >&2; }
|
||||
log_error() { printf '%s%s%s%s\n' "${__C_ERR}" "${SCRIPT_TAG:+[$SCRIPT_TAG] }" "error: $*" "${__C_RESET}" >&2; }
|
||||
@@ -0,0 +1,27 @@
|
||||
# shellcheck shell=bash
|
||||
# set-wallpaper — bind the runtime wallpaper symlink to $1 and restart hyprpaper
|
||||
# so it picks up the new image. hyprpaper's config (managed by Home Manager)
|
||||
# points at the symlink, so we never write hyprpaper config ourselves.
|
||||
#
|
||||
# Env (injected by the Nix wrapper):
|
||||
# wallpaperSymlink path to the symlink hyprpaper's `wallpaper.path` reads
|
||||
# Arg: image path
|
||||
|
||||
: "${wallpaperSymlink:?set-wallpaper: wallpaperSymlink not set}"
|
||||
set -euo pipefail
|
||||
|
||||
target="${1:?usage: set-wallpaper <image-path>}"
|
||||
[ -f "$target" ] || { log_error "not a file: $target"; exit 1; }
|
||||
|
||||
mkdir -p "$(dirname "$wallpaperSymlink")"
|
||||
ln -sfn "$target" "$wallpaperSymlink"
|
||||
|
||||
# Apply live by (re)starting the user hyprpaper service. `systemctl restart`
|
||||
# starts the unit even if it is inactive — important because a freshly-added
|
||||
# hyprpaper service won't be running until the first switch. Tolerate the
|
||||
# service being absent (e.g. on a host without hyprpaper).
|
||||
if command -v systemctl >/dev/null 2>&1 && systemctl --user restart hyprpaper 2>/dev/null; then
|
||||
log_info "applied $(basename "$target") (hyprpaper started)"
|
||||
else
|
||||
log_warn "could not start hyprpaper; wallpaper symlink set but not displayed"
|
||||
fi
|
||||
@@ -57,7 +57,7 @@ do
|
||||
done
|
||||
|
||||
if [ -z "$gen" ]; then
|
||||
echo "error: could not resolve Home Manager base generation" >&2
|
||||
log_error "could not resolve Home Manager base generation"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -72,18 +72,16 @@ case "$variant" in
|
||||
esac
|
||||
|
||||
if [ ! -x "$activate" ]; then
|
||||
echo "error: variant '$variant' not found at $activate" >&2
|
||||
echo "Available: ${availableVariants}"
|
||||
log_error "variant '$variant' not found at $activate"
|
||||
log_info "available: ${availableVariants}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Switching theme to $variant..."
|
||||
log_info "switching theme to ${variant}"
|
||||
|
||||
# Best-effort: live-switch the running Doom Emacs daemon too. No config
|
||||
# files are touched; if the daemon isn't running or emacsclient isn't
|
||||
# installed (e.g. on a server), this is silently skipped. A theme symbol
|
||||
# that isn't installed in Doom makes emacsclient return non-zero, which we
|
||||
# also treat as skip.
|
||||
# installed (e.g. on a server), this is silently skipped.
|
||||
doom_theme=""
|
||||
if [ -n "$doomThemes" ]; then
|
||||
for pair in $doomThemes; do
|
||||
@@ -92,12 +90,26 @@ if [ -n "$doomThemes" ]; then
|
||||
esac
|
||||
done
|
||||
fi
|
||||
if [ -n "$doom_theme" ] && command -v emacsclient >/dev/null 2>&1; then
|
||||
if emacsclient --eval "(progn (mapc (function disable-theme) custom-enabled-themes) (load-theme (quote ${doom_theme}) t))" >/dev/null 2>&1; then
|
||||
echo "Doom: ${doom_theme}"
|
||||
if [ -n "$doom_theme" ]; then
|
||||
if command -v emacsclient >/dev/null 2>&1 \
|
||||
&& emacsclient --eval "(progn (mapc (function disable-theme) custom-enabled-themes) (load-theme (quote ${doom_theme}) t))" >/dev/null 2>&1; then
|
||||
log_info "doom: ${doom_theme}"
|
||||
else
|
||||
echo "note: Doom theme switch skipped (daemon not running or '${doom_theme}' not installed)" >&2
|
||||
log_warn "doom theme not applied (daemon not running or '${doom_theme}' not installed)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Pick a palette-matching wallpaper and apply it. This BLOCKS — on the very
|
||||
# first run wallpaper-pick clones the wallpaper repo and builds the palette
|
||||
# index, which can take a few minutes; progress is logged on stderr by the
|
||||
# pick/set scripts. Failures fall back to leaving the wallpaper unchanged.
|
||||
if [ "${wallpapersEnabled:-0}" = "1" ] && [ -n "${wallpaperPickBin:-}" ] && [ -n "${wallpaperSetBin:-}" ]; then
|
||||
if wp="$("$wallpaperPickBin" "$variant")" && [ -n "$wp" ]; then
|
||||
"$wallpaperSetBin" "$wp" || log_warn "wallpaper apply failed"
|
||||
else
|
||||
log_warn "wallpaper selection failed; leaving wallpaper unchanged"
|
||||
fi
|
||||
fi
|
||||
|
||||
log_info "activating Home Manager specialisation"
|
||||
exec "$activate"
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
# shellcheck shell=bash
|
||||
# wallpaper-pick — print a random wallpaper from $wallpapersDir whose palette
|
||||
# best matches the given theme variant. Per-image palettes are extracted once
|
||||
# (ImageMagick) and cached, keyed by the (immutable) source path so a rebuild
|
||||
# that changes the wallpaper set reindexes automatically.
|
||||
#
|
||||
# Matching uses several key colors on both sides: the variant's base16
|
||||
# base00–base05 (the background→foreground neutral range) are each matched to
|
||||
# their nearest wallpaper dominant color; the sum of those nearest distances
|
||||
# is the score. A random image is picked from the lowest-score tier.
|
||||
#
|
||||
# Env (injected by the Nix wrapper):
|
||||
# wallpapersDir directory of wallpaper images (~/Wallpapers, a git clone)
|
||||
# wallpaperRepo git URL cloned into wallpapersDir on first use
|
||||
# variantSchemes space-separated `variant:schemeFile` pairs (base16 YAML)
|
||||
# MAGICK path to the ImageMagick `magick` binary
|
||||
# GIT path to the `git` binary
|
||||
# Arg: variant name (e.g. gruvbox-dark)
|
||||
|
||||
: "${wallpapersDir:?wallpaper-pick: wallpapersDir not set}"
|
||||
: "${wallpaperRepo:?wallpaper-pick: wallpaperRepo not set}"
|
||||
: "${variantSchemes:?wallpaper-pick: variantSchemes not set}"
|
||||
: "${MAGICK:?wallpaper-pick: MAGICK not set}"
|
||||
: "${GIT:?wallpaper-pick: GIT not set}"
|
||||
set -euo pipefail
|
||||
|
||||
variant="${1:?usage: wallpaper-pick <variant>}"
|
||||
|
||||
# Ensure the wallpaper repo is cloned locally. The clone is a one-time cost
|
||||
# paid on first use (not at build time), keeping `nix build` fast.
|
||||
if [ ! -d "$wallpapersDir/.git" ]; then
|
||||
log_info "cloning wallpaper repo (one-time, may take a few minutes)..."
|
||||
mkdir -p "$(dirname "$wallpapersDir")"
|
||||
"$GIT" clone --depth=1 --single-branch "$wallpaperRepo" "$wallpapersDir" >&2
|
||||
log_info "clone ready"
|
||||
fi
|
||||
|
||||
cache_dir="${XDG_CACHE_HOME:-$HOME/.cache}/wallpaper-pick"
|
||||
palettes="$cache_dir/palettes.tsv"
|
||||
stamp_file="$cache_dir/source"
|
||||
|
||||
# Cache key: the clone's current commit, so a `git pull` reindexes. Falls back
|
||||
# to the resolved path if git fails for some reason.
|
||||
src_stamp="$("$GIT" -C "$wallpapersDir" rev-parse HEAD 2>/dev/null || readlink -f "$wallpapersDir")"
|
||||
|
||||
rebuild_cache() {
|
||||
img_count="$(find "$wallpapersDir" -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \) -printf . 2>/dev/null | wc -c)"
|
||||
log_info "indexing ${img_count} wallpapers (one-time, building palette cache)..."
|
||||
mkdir -p "$cache_dir"
|
||||
tmp="$palettes.tmp"
|
||||
: > "$tmp"
|
||||
jobs="$(nproc 2>/dev/null || echo 4)"
|
||||
|
||||
# For each image: top dominant colors (by pixel count) as space-separated #hex.
|
||||
find "$wallpapersDir" -type f \( -iname '*.png' -o -iname '*.jpg' -o -iname '*.jpeg' -o -iname '*.webp' \) -print0 \
|
||||
| xargs -0 -P "$jobs" -n1 sh -c '
|
||||
MAGICK="$0" img="$1"
|
||||
"$MAGICK" "$img" -resize 128x128^ -gravity center -extent 128x128 +dither -colors 8 -format "%c" histogram:info: 2>/dev/null \
|
||||
| grep -E "^[[:space:]]*[0-9]+:" | sort -rn | grep -oE "#[0-9A-Fa-f]{6}" | head -6 | paste -sd" " - \
|
||||
| { read -r hex; [ -n "$hex" ] && printf "%s\t%s\n" "$img" "$hex"; }
|
||||
' "$MAGICK" >> "$tmp" || true
|
||||
|
||||
mv "$tmp" "$palettes"
|
||||
printf '%s\n' "$src_stamp" > "$stamp_file"
|
||||
}
|
||||
|
||||
if [ ! -f "$palettes" ] || [ ! -f "$stamp_file" ] || [ "$(cat "$stamp_file")" != "$src_stamp" ]; then
|
||||
rebuild_cache
|
||||
fi
|
||||
|
||||
# Resolve the base16 scheme file for the requested variant.
|
||||
scheme=""
|
||||
for pair in $variantSchemes; do
|
||||
case "$pair" in
|
||||
"${variant}":*) scheme="${pair#*:}"; break ;;
|
||||
esac
|
||||
done
|
||||
[ -n "$scheme" ] || { log_error "no scheme mapped for variant '$variant'"; exit 2; }
|
||||
|
||||
# Theme key colors: base00–base05 hex (one per line, with leading #).
|
||||
theme_colors="$(grep -E '^[[:space:]]*base0[0-5]:' "$scheme" | grep -oE '#[0-9A-Fa-f]{6}')"
|
||||
[ -n "$theme_colors" ] || { log_error "could not parse base00–05 from $scheme"; exit 2; }
|
||||
|
||||
# hex (no #) -> decimal rgb on stdout
|
||||
hex2dec() { printf '%d %d %d\n' "$((16#${1:0:2}))" "$((16#${1:2:2}))" "$((16#${1:4:2}))"; }
|
||||
|
||||
# Theme colors as parallel arrays.
|
||||
t_r=(); t_g=(); t_b=()
|
||||
while read -r h; do
|
||||
read -r r g b < <(hex2dec "${h#\#}")
|
||||
t_r+=("$r"); t_g+=("$g"); t_b+=("$b")
|
||||
done <<< "$theme_colors"
|
||||
|
||||
# Score every indexed wallpaper: sum over theme colors of nearest wallpaper color.
|
||||
scores="$cache_dir/scores.tmp"
|
||||
: > "$scores"
|
||||
while IFS=$'\t' read -r path whexes; do
|
||||
w_r=(); w_g=(); w_b=()
|
||||
for wh in $whexes; do
|
||||
read -r wr wg wb < <(hex2dec "${wh#\#}")
|
||||
w_r+=("$wr"); w_g+=("$wg"); w_b+=("$wb")
|
||||
done
|
||||
if [ "${#w_r[@]}" -eq 0 ]; then continue; fi
|
||||
total=0
|
||||
for i in "${!t_r[@]}"; do
|
||||
mind=1000000000
|
||||
for j in "${!w_r[@]}"; do
|
||||
dr=$(( t_r[i] - w_r[j] )); dg=$(( t_g[i] - w_g[j] )); db=$(( t_b[i] - w_b[j] ))
|
||||
cd=$(( dr*dr + dg*dg + db*db ))
|
||||
if [ "$cd" -lt "$mind" ]; then mind=$cd; fi
|
||||
done
|
||||
total=$(( total + mind ))
|
||||
done
|
||||
printf '%s\t%s\n' "$total" "$path"
|
||||
done < "$palettes" | sort -n -k1 > "$scores"
|
||||
|
||||
[ -s "$scores" ] || { log_error "no wallpapers indexed"; exit 3; }
|
||||
|
||||
# Matched tier: within 1.4x of the best score (fall back to closest 20).
|
||||
best="$(head -n1 "$scores" | cut -f1)"
|
||||
threshold=$(( best * 14 / 10 ))
|
||||
matched="$(awk -F'\t' -v t="$threshold" '$1 <= t {print $2}' "$scores")"
|
||||
if [ "$(printf '%s\n' "$matched" | grep -c .)" -lt 5 ]; then
|
||||
matched="$(head -n20 "$scores" | cut -f2)"
|
||||
fi
|
||||
|
||||
# Print the chosen PATH on stdout (data channel for callers) and log the
|
||||
# human-readable choice on stderr.
|
||||
chosen="$(printf '%s\n' "$matched" | shuf -n1)"
|
||||
log_info "picked $(basename "$chosen") for ${variant}"
|
||||
printf '%s\n' "$chosen"
|
||||
@@ -70,7 +70,10 @@
|
||||
|
||||
notification.mako.enable = true;
|
||||
|
||||
dynamic-theme.enable = true;
|
||||
dynamic-theme = {
|
||||
enable = true;
|
||||
wallpaperRepo = "https://gitea.susano-homelab.duckdns.org/fujin/gruvbox-wallpapers.git";
|
||||
};
|
||||
};
|
||||
|
||||
programs = {
|
||||
@@ -88,13 +91,6 @@
|
||||
directory = ["/home/${username}/nixos-dotfiles" "/home/${username}/.cache/nix"];
|
||||
};
|
||||
};
|
||||
hooks = {
|
||||
commit-msg = pkgs.writeScript "commit-msg" ''
|
||||
#!${pkgs.bash}/bin/bash
|
||||
# Remove Co-Authored-by lines from commit messages (Claude likes to add these)
|
||||
${pkgs.gnused}/bin/sed -i '/^Co-[Aa]uthored-[Bb]y:/d' "$1"
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user