Switch the image picker to IpcHandler
This commit is contained in:
+17
-41
@@ -83,8 +83,6 @@ selection_file=$(mktemp)
|
|||||||
done_file=$(mktemp)
|
done_file=$(mktemp)
|
||||||
rm -f "$done_file"
|
rm -f "$done_file"
|
||||||
trap 'rm -f "$selection_file" "$done_file"' EXIT
|
trap 'rm -f "$selection_file" "$done_file"' EXIT
|
||||||
socket_path="${XDG_RUNTIME_DIR:-/run/user/$UID}/omarchy-image-selector.sock"
|
|
||||||
shell_dir="$OMARCHY_PATH/default/quickshell/omarchy-shell"
|
|
||||||
|
|
||||||
image_dirs_env=""
|
image_dirs_env=""
|
||||||
for dir in "${image_dirs[@]}"; do
|
for dir in "${image_dirs[@]}"; do
|
||||||
@@ -214,59 +212,37 @@ else
|
|||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
rows_payload=${rows//$'\t'/$'\f'}
|
colors_file=${colors_file:-$HOME/.config/omarchy/current/theme/image-picker-colors.json}
|
||||||
rows_payload=${rows_payload//$'\n'/$'\v'}
|
|
||||||
colors_file=${colors_file:-$HOME/.config/omarchy/current/theme/background-switcher-colors.json}
|
|
||||||
colors_payload=""
|
colors_payload=""
|
||||||
|
|
||||||
if [[ -f $colors_file ]]; then
|
if [[ -f $colors_file ]]; then
|
||||||
colors_payload=$(<"$colors_file")
|
colors_payload=$(<"$colors_file")
|
||||||
colors_payload=${colors_payload//$'\t'/$'\f'}
|
|
||||||
colors_payload=${colors_payload//$'\n'/$'\v'}
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ $cache_only == true || $prepare_only == true ]]; then
|
if [[ $cache_only == true || $prepare_only == true ]]; then
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ensure_selector() {
|
# Image rows and the raw colors blob can contain newlines and tabs, which
|
||||||
# The image-selector socket is owned by omarchy-shell. Make sure the shell is
|
# don't survive a positional bash argv into `quickshell ipc call`. Base64
|
||||||
# alive; once it is, the BackgroundSwitcher plugin keeps the socket bound for
|
# them; the ImagePicker plugin Qt.atob()s on the other side.
|
||||||
# the lifetime of the shell.
|
rows_b64=$(printf '%s' "$rows" | base64 -w 0)
|
||||||
if [[ ! -S $socket_path ]]; then
|
colors_b64=$(printf '%s' "$colors_payload" | base64 -w 0)
|
||||||
omarchy-shell-ipc shell ping >/dev/null 2>&1 || true
|
|
||||||
|
|
||||||
for ((i = 0; i < 100; i++)); do
|
if ! omarchy-shell-ipc image-selector open \
|
||||||
if [[ -S $socket_path ]]; then
|
"" \
|
||||||
break
|
"$rows_b64" \
|
||||||
fi
|
"$selected_list_image" \
|
||||||
|
"$selection_file" \
|
||||||
sleep 0.01
|
"$done_file" \
|
||||||
done
|
"" \
|
||||||
fi
|
"$colors_b64" \
|
||||||
|
"$show_labels" \
|
||||||
[[ -S $socket_path ]]
|
"$filterable" >/dev/null; then
|
||||||
}
|
echo "Image selector failed to accept request" >&2
|
||||||
|
|
||||||
send_request() {
|
|
||||||
printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$rows_payload" "$selected_list_image" "$selection_file" "$done_file" "$colors_payload" "$show_labels" "$filterable" |
|
|
||||||
socat -u - "UNIX-CONNECT:$socket_path"
|
|
||||||
}
|
|
||||||
|
|
||||||
if ! ensure_selector; then
|
|
||||||
echo "Image selector failed to start" >&2
|
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! send_request; then
|
|
||||||
rm -f "$socket_path"
|
|
||||||
|
|
||||||
if ! ensure_selector || ! send_request; then
|
|
||||||
echo "Image selector failed to accept request" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
while [[ ! -e $done_file ]]; do
|
while [[ ! -e $done_file ]]; do
|
||||||
sleep 0.01
|
sleep 0.01
|
||||||
done
|
done
|
||||||
|
|||||||
@@ -12,9 +12,11 @@ Starts omarchy-shell if not running, then forwards a quickshell ipc call.
|
|||||||
Examples:
|
Examples:
|
||||||
omarchy-shell-ipc shell ping
|
omarchy-shell-ipc shell ping
|
||||||
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
omarchy-shell-ipc shell summon omarchy.bar-settings "{}"
|
||||||
omarchy-shell-ipc shell hide omarchy.background-switcher
|
omarchy-shell-ipc shell hide omarchy.image-picker
|
||||||
omarchy-shell-ipc shell listPlugins
|
omarchy-shell-ipc shell listPlugins
|
||||||
omarchy-shell-ipc shell rescanPlugins
|
omarchy-shell-ipc shell rescanPlugins
|
||||||
|
omarchy-shell-ipc image-selector ping
|
||||||
|
omarchy-shell-ipc image-selector cancel ""
|
||||||
USAGE
|
USAGE
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -28,7 +30,10 @@ lockfile="${XDG_RUNTIME_DIR:-/tmp}/omarchy-shell-ipc.lock"
|
|||||||
{
|
{
|
||||||
flock 9
|
flock 9
|
||||||
if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then
|
if ! quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance '; then
|
||||||
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 &
|
# 9<&- so the spawned shell does not inherit (and keep) the lock fd; the
|
||||||
|
# spawned process is long-lived and would otherwise hold the lock for the
|
||||||
|
# remainder of the session, deadlocking every subsequent helper invocation.
|
||||||
|
setsid uwsm-app -- env OMARCHY_PATH="$OMARCHY_PATH" quickshell -p "$SHELL_DIR" >/dev/null 2>&1 9<&- &
|
||||||
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
for _ in 1 2 3 4 5 6 7 8 9 10; do
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break
|
quickshell list -p "$SHELL_DIR" 2>/dev/null | grep -q '^Instance ' && break
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ default/quickshell/omarchy-shell/
|
|||||||
plugins/
|
plugins/
|
||||||
bar/ first-party plugins (see plugins/README.md)
|
bar/ first-party plugins (see plugins/README.md)
|
||||||
bar-settings/
|
bar-settings/
|
||||||
background-switcher/
|
image-picker/
|
||||||
```
|
```
|
||||||
|
|
||||||
The plugin discovery path is documented in [plugins/README.md](plugins/README.md).
|
The plugin discovery path is documented in [plugins/README.md](plugins/README.md).
|
||||||
@@ -73,8 +73,9 @@ Supported `kinds`:
|
|||||||
|
|
||||||
`activation` is either `persistent` (loaded on startup, never unloaded) or
|
`activation` is either `persistent` (loaded on startup, never unloaded) or
|
||||||
`on-demand` (loaded by `shell summon <id>` and unloaded by `shell hide`).
|
`on-demand` (loaded by `shell summon <id>` and unloaded by `shell hide`).
|
||||||
Plugins that need their IPC socket to outlive a single summon can set
|
Plugins that need to outlive a single summon can set `keepLoaded: true`
|
||||||
`keepLoaded: true` (e.g. background-switcher's legacy unix socket).
|
(e.g. the image picker keeps its overlay window mounted between
|
||||||
|
summons).
|
||||||
|
|
||||||
The full schema lives in `services/PluginRegistry.qml`.
|
The full schema lives in `services/PluginRegistry.qml`.
|
||||||
|
|
||||||
@@ -147,7 +148,9 @@ Built up in phases on this branch:
|
|||||||
- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry`
|
- Phase 2 — `omarchy-shell phase 2: plugin registry and bar widget registry`
|
||||||
- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin`
|
- Phase 3 — `omarchy-shell phase 3: fold bar-settings into the shell as a panel plugin`
|
||||||
- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin`
|
- Phase 4 — `omarchy-shell phase 4: absorb background-switcher as a plugin`
|
||||||
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs` (this commit)
|
- Phase 5 — `omarchy-shell phase 5: docs, cleanup, and migration crumbs`
|
||||||
|
- Phase 6 — `omarchy-shell phase 6: reviewer cleanup (path traversal, collision, races)`
|
||||||
|
- Phase 7 — `omarchy-shell phase 7: replace socket with IpcHandler, rename to image-picker`
|
||||||
|
|
||||||
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
Shared services and Pipewire/UPower/Hyprland consolidation are explicitly
|
||||||
out of scope here and deferred to a follow-up after a review pass.
|
out of scope here and deferred to a follow-up after a review pass.
|
||||||
|
|||||||
@@ -8,11 +8,11 @@ so they cannot be disabled.
|
|||||||
User-installed plugins live alongside these conceptually but on disk under
|
User-installed plugins live alongside these conceptually but on disk under
|
||||||
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
|
`~/.config/omarchy/plugins/<plugin-id>/` rather than in this directory.
|
||||||
|
|
||||||
| Plugin | id | kinds | activation | entry point |
|
| Plugin | id | kinds | activation | entry point |
|
||||||
|-----------------------|-------------------------------|--------------|-------------|----------------------------|
|
|------------------|--------------------------|--------------|-------------|----------------------------------------------|
|
||||||
| Bar | `omarchy.bar` | `bar` | persistent | `bar/Bar.qml` |
|
| Bar | `omarchy.bar` | `bar` | persistent | `bar/Bar.qml` |
|
||||||
| Bar settings | `omarchy.bar-settings` | `panel` | on-demand | `bar-settings/BarSettingsPanel.qml` |
|
| Bar settings | `omarchy.bar-settings` | `panel` | on-demand | `bar-settings/BarSettingsPanel.qml` |
|
||||||
| Background switcher | `omarchy.background-switcher` | `overlay` | on-demand | `background-switcher/BackgroundSwitcher.qml` |
|
| Image picker | `omarchy.image-picker` | `overlay` | on-demand | `image-picker/ImagePicker.qml` |
|
||||||
|
|
||||||
## Bar
|
## Bar
|
||||||
|
|
||||||
@@ -33,13 +33,31 @@ Visual editor for the bar layout. Summoned by
|
|||||||
- a Plugin Manager tab for enabling/disabling third-party plugins
|
- a Plugin Manager tab for enabling/disabling third-party plugins
|
||||||
- a dynamic settings form driven by each widget's manifest schema
|
- a dynamic settings form driven by each widget's manifest schema
|
||||||
|
|
||||||
## Background switcher
|
## Image picker
|
||||||
|
|
||||||
Fullscreen wallpaper / image picker overlay. Summoned for ad-hoc wallpaper
|
Fullscreen image-grid selector overlay. Used by `omarchy-menu-images`
|
||||||
selection. Keeps its legacy unix socket protocol at
|
(wallpaper picker) and `omarchy-theme-switcher` (theme picker) and any
|
||||||
`/run/user/<uid>/omarchy-image-selector.sock` so existing callers like
|
other caller that wants to present a directory of images with previews.
|
||||||
`omarchy-menu-images` keep working without any wire-format change. The
|
|
||||||
plugin has `keepLoaded: true` so the socket survives between summons.
|
Two ways to drive it:
|
||||||
|
|
||||||
|
- Shell-level summon: `omarchy-shell-ipc shell summon omarchy.image-picker '<jsonPayload>'`.
|
||||||
|
The payload can carry `imageDirs`, `imageRows`, `selectedImage`,
|
||||||
|
`selectionFile`, `doneFile`, `colorsFile`, `colorsRaw`, `showLabels`,
|
||||||
|
`filterable`. Best for in-shell callers that already speak JSON.
|
||||||
|
- Direct IPC target: `omarchy-shell-ipc image-selector open <imageDirs> <imageRowsB64> <selectedImage> <selectionFile> <doneFile> <colorsFile> <colorsRawB64> <showLabels> <filterable>`.
|
||||||
|
Positional args; `imageRowsB64` and `colorsRawB64` are base64-encoded so
|
||||||
|
embedded newlines / tabs survive the bash argv handoff. This is what
|
||||||
|
`omarchy-menu-images` uses.
|
||||||
|
|
||||||
|
The selection round-trip remains file-based: callers create a
|
||||||
|
`selection_file` and `done_file` (both `mktemp`), pass the paths, and
|
||||||
|
poll `done_file` for existence. The plugin writes the chosen path into
|
||||||
|
`selection_file` and touches `done_file` when it's done. `cancel` IPC
|
||||||
|
clears it without writing a selection.
|
||||||
|
|
||||||
|
The plugin has `keepLoaded: true` so the layer-shell window survives
|
||||||
|
between summons within a single shell session.
|
||||||
|
|
||||||
## Coming soon
|
## Coming soon
|
||||||
|
|
||||||
|
|||||||
@@ -1,16 +0,0 @@
|
|||||||
{
|
|
||||||
"schemaVersion": 1,
|
|
||||||
"id": "omarchy.background-switcher",
|
|
||||||
"name": "Background switcher",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"author": "Omarchy",
|
|
||||||
"description": "Wallpaper / image picker overlay",
|
|
||||||
"kinds": ["overlay"],
|
|
||||||
"activation": "on-demand",
|
|
||||||
"keepLoaded": true,
|
|
||||||
"entryPoints": { "overlay": "BackgroundSwitcher.qml" },
|
|
||||||
"ipc": {
|
|
||||||
"summon": "background-switcher",
|
|
||||||
"legacySocket": "omarchy-image-selector.sock"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+40
-36
@@ -22,7 +22,7 @@ Item {
|
|||||||
property string imageRows: ""
|
property string imageRows: ""
|
||||||
property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE")
|
property string selectionFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTION_FILE") || Quickshell.env("OMARCHY_BACKGROUND_SELECTION_FILE")
|
||||||
property string selectedImage: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTED")
|
property string selectedImage: Quickshell.env("OMARCHY_IMAGE_SELECTOR_SELECTED")
|
||||||
property string colorsFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json")
|
property string colorsFile: Quickshell.env("OMARCHY_IMAGE_SELECTOR_COLORS_FILE") || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/image-picker-colors.json")
|
||||||
property int selectedIndex: 0
|
property int selectedIndex: 0
|
||||||
property bool imagesLoaded: false
|
property bool imagesLoaded: false
|
||||||
property bool opened: false
|
property bool opened: false
|
||||||
@@ -34,7 +34,6 @@ Item {
|
|||||||
property string doneFile: ""
|
property string doneFile: ""
|
||||||
property string filterText: ""
|
property string filterText: ""
|
||||||
property var doneFilesToRelease: []
|
property var doneFilesToRelease: []
|
||||||
property string socketPath: (Quickshell.env("XDG_RUNTIME_DIR") || ("/run/user/" + Quickshell.env("UID"))) + "/omarchy-image-selector.sock"
|
|
||||||
property color accent: "#798186"
|
property color accent: "#798186"
|
||||||
property color background: "#101315"
|
property color background: "#101315"
|
||||||
property color foreground: "#cacccc"
|
property color foreground: "#cacccc"
|
||||||
@@ -53,8 +52,13 @@ Item {
|
|||||||
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
||||||
}
|
}
|
||||||
|
|
||||||
function decodeField(value) {
|
// Decode a base64-encoded UTF-8 string sent via IPC. Used for fields that
|
||||||
return String(value || "").replace(/\v/g, "\n").replace(/\f/g, "\t")
|
// would otherwise carry embedded newlines or tabs (image rows, raw colors
|
||||||
|
// JSON) which bash IPC arguments can't reliably round-trip.
|
||||||
|
function decodeBase64(value) {
|
||||||
|
var s = String(value || "")
|
||||||
|
if (!s) return ""
|
||||||
|
try { return Qt.atob(s) } catch (e) { return s }
|
||||||
}
|
}
|
||||||
|
|
||||||
function withAlpha(color, alpha) {
|
function withAlpha(color, alpha) {
|
||||||
@@ -251,7 +255,7 @@ Item {
|
|||||||
showLabels = nextShowLabels === true || nextShowLabels === "true"
|
showLabels = nextShowLabels === true || nextShowLabels === "true"
|
||||||
filterable = nextFilterable === true || nextFilterable === "true"
|
filterable = nextFilterable === true || nextFilterable === "true"
|
||||||
filterText = ""
|
filterText = ""
|
||||||
colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/background-switcher-colors.json")
|
colorsFile = nextColorsFile || (Quickshell.env("HOME") + "/.config/omarchy/current/theme/image-picker-colors.json")
|
||||||
if (nextColorsRaw)
|
if (nextColorsRaw)
|
||||||
loadColors(nextColorsRaw)
|
loadColors(nextColorsRaw)
|
||||||
imageModel.clear()
|
imageModel.clear()
|
||||||
@@ -303,16 +307,11 @@ Item {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The plugin is keep-loaded inside omarchy-shell, so the env-driven
|
// Lifecycle hooks invoked by omarchy-shell summon/hide. shell.summon(id,
|
||||||
// auto-open path that the standalone background-switcher.qml used would
|
// payloadJson) hands the JSON to open() here; shell.hide(id) calls close().
|
||||||
// now fire once at shell startup with stale env. The legacy callers
|
// External CLI callers can either go through `shell summon omarchy.image-
|
||||||
// (omarchy-menu-images) deliver their request over the unix socket
|
// picker` (JSON payload), or hit the dedicated `image-selector` IpcHandler
|
||||||
// declared below; modern callers go through `shell summon` -> open(payload).
|
// below for the lower-level positional call that omarchy-menu-images uses.
|
||||||
|
|
||||||
// Lifecycle hooks invoked by omarchy-shell summon/hide. The legacy entry
|
|
||||||
// point remains the unix socket below — callers that already have a
|
|
||||||
// selection_file/done_file flow keep using it. summon() with no payload
|
|
||||||
// simply opens the picker against the user's current theme backgrounds.
|
|
||||||
function open(payload) {
|
function open(payload) {
|
||||||
var args = {}
|
var args = {}
|
||||||
if (payload) {
|
if (payload) {
|
||||||
@@ -334,39 +333,44 @@ Item {
|
|||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// IPC surface. All arguments are strings (Quickshell IPC marshalling).
|
||||||
|
// imageRows and colorsRaw can contain newlines/tabs, so the CLI caller
|
||||||
|
// base64-encodes them; everything else passes through verbatim. The two
|
||||||
|
// boolean-like fields use the literal strings "true" or "false".
|
||||||
IpcHandler {
|
IpcHandler {
|
||||||
target: "image-selector"
|
target: "image-selector"
|
||||||
|
|
||||||
function open(imageDirs: string, imageRows: string, selectedImage: string, selectionFile: string, doneFile: string, colorsFile: string): void {
|
function open(imageDirs: string,
|
||||||
root.openSelector(imageDirs, imageRows, selectedImage, selectionFile, doneFile, colorsFile, "", false, false)
|
imageRowsB64: string,
|
||||||
|
selectedImage: string,
|
||||||
|
selectionFile: string,
|
||||||
|
doneFile: string,
|
||||||
|
colorsFile: string,
|
||||||
|
colorsRawB64: string,
|
||||||
|
showLabels: string,
|
||||||
|
filterable: string): string {
|
||||||
|
var rows = root.decodeBase64(imageRowsB64)
|
||||||
|
var colorsRaw = root.decodeBase64(colorsRawB64)
|
||||||
|
root.openSelector(imageDirs, rows, selectedImage, selectionFile, doneFile,
|
||||||
|
colorsFile, colorsRaw, showLabels, filterable)
|
||||||
|
return "ok"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
SocketServer {
|
function cancel(doneFile: string): void {
|
||||||
active: true
|
root.closeSelector(doneFile || "")
|
||||||
path: root.socketPath
|
}
|
||||||
|
|
||||||
handler: Socket {
|
|
||||||
id: clientSocket
|
|
||||||
parser: SplitParser {
|
|
||||||
onRead: function(message) {
|
|
||||||
var fields = message.split("\t")
|
|
||||||
if (root.opened) {
|
|
||||||
root.closeSelector(fields[3] || "")
|
|
||||||
clientSocket.connected = false
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
root.openSelector("", root.decodeField(fields[0]), fields[1] || "", fields[2] || "", fields[3] || "", "", root.decodeField(fields[4]), fields[5] || "false", fields[6] || "false")
|
function ping(): string {
|
||||||
clientSocket.connected = false
|
return "ok"
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Tolerate the file being absent (e.g. theme templates not yet re-rendered
|
||||||
|
// after the rename) without a startup warning.
|
||||||
FileView {
|
FileView {
|
||||||
path: root.colorsFile
|
path: root.colorsFile
|
||||||
watchChanges: true
|
watchChanges: true
|
||||||
|
printErrors: false
|
||||||
onLoaded: root.loadColors(text())
|
onLoaded: root.loadColors(text())
|
||||||
onFileChanged: { reload(); root.loadColors(text()) }
|
onFileChanged: { reload(); root.loadColors(text()) }
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"schemaVersion": 1,
|
||||||
|
"id": "omarchy.image-picker",
|
||||||
|
"name": "Image picker",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"author": "Omarchy",
|
||||||
|
"description": "Image-grid selector overlay used for wallpapers, themes, and any other directory of images",
|
||||||
|
"kinds": ["overlay"],
|
||||||
|
"activation": "on-demand",
|
||||||
|
"keepLoaded": true,
|
||||||
|
"entryPoints": { "overlay": "ImagePicker.qml" },
|
||||||
|
"ipc": {
|
||||||
|
"summon": "image-picker"
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user