mirror of
https://github.com/zoriya/flake.git
synced 2026-06-05 19:45:58 +00:00
Add audio and sink selector in quicksettings
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
// import * as audio from "../modules/audio.js";
|
||||
import Gtk from "gi://Gtk?version=3.0";
|
||||
import * as audio from "../modules/audio.js";
|
||||
// import * as brightness from "../modules/brightness.js";
|
||||
// import * as network from "../modules/network.js";
|
||||
// import * as bluetooth from "../modules/bluetooth.js";
|
||||
@@ -18,7 +19,7 @@ const mprisService = await Service.import("mpris");
|
||||
// [opened, (r) => (r.reveal_child = menuName === opened.value)],
|
||||
// ],
|
||||
// child: Box({
|
||||
// className: "qs-submenu surface",
|
||||
// className: "",
|
||||
// vertical: true,
|
||||
// children: [
|
||||
// Box({
|
||||
@@ -29,33 +30,24 @@ const mprisService = await Service.import("mpris");
|
||||
// ],
|
||||
// }),
|
||||
// });
|
||||
//
|
||||
// const VolumeBox = () =>
|
||||
// Box({
|
||||
// vertical: true,
|
||||
// children: [
|
||||
// Box({
|
||||
// className: "qs-slider",
|
||||
// children: [
|
||||
// Button({
|
||||
// child: audio.SpeakerIndicator(),
|
||||
// onClicked: () =>
|
||||
// execAsync("pactl set-sink-mute @DEFAULT_SINK@ toggle"),
|
||||
// }),
|
||||
// audio.SpeakerSlider({ hexpand: true }),
|
||||
// audio.SpeakerPercentLabel(),
|
||||
// Arrow({ name: "stream-selector" }),
|
||||
// ],
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "stream-selector",
|
||||
// icon: Icon("audio-volume-medium-symbolic"),
|
||||
// title: "Audio Stream",
|
||||
// contentType: audio.StreamSelector,
|
||||
// }),
|
||||
// ],
|
||||
// });
|
||||
//
|
||||
|
||||
/**
|
||||
* @param {Array<Gtk.Widget>} toggles
|
||||
* @param {Array<Gtk.Widget>} menus
|
||||
*/
|
||||
const Row = (toggles = [], menus = []) =>
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
homogeneous: true,
|
||||
class_name: "row horizontal",
|
||||
children: toggles,
|
||||
}),
|
||||
...menus,
|
||||
],
|
||||
});
|
||||
|
||||
// const BrightnessBox = () =>
|
||||
// Box({
|
||||
// className: "qs-slider",
|
||||
@@ -76,40 +68,41 @@ export const Quicksettings = () =>
|
||||
child: Widget.Box({
|
||||
vertical: true,
|
||||
className: "bgcont qs-container",
|
||||
children:
|
||||
mprisService
|
||||
.bind("players")
|
||||
.as((x) => x.map((player) => mpris.MprisPlayer({ player }))),
|
||||
// children: [
|
||||
// VolumeBox(),
|
||||
// BrightnessBox(),
|
||||
// Widget.Box({
|
||||
// children: [network.Toggle({}), bluetooth.Toggle({})],
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "network",
|
||||
// icon: "network-wireless-symbolic",
|
||||
// title: "Network",
|
||||
// contentType: network.Selection,
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "bluetooth",
|
||||
// icon: "bluetooth-symbolic",
|
||||
// title: "Bluetooth",
|
||||
// contentType: bluetooth.Devices,
|
||||
// }),
|
||||
// Box({
|
||||
// children: [darkmode.DarkToggle(), nightmode.NightToggle()],
|
||||
// }),
|
||||
// Box({
|
||||
// children: [audio.AppMixerToggle(), audio.MuteToggle()],
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "app-mixer",
|
||||
// icon: FontIcon({ icon: "" }),
|
||||
// title: "App Mixer",
|
||||
// contentType: audio.AppMixer,
|
||||
// }),
|
||||
// ],
|
||||
children: [
|
||||
Row([audio.Volume({ type: "speaker" })], [audio.SinkSelector({})]),
|
||||
// BrightnessBox(),
|
||||
// Widget.Box({
|
||||
// children: [network.Toggle({}), bluetooth.Toggle({})],
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "network",
|
||||
// icon: "network-wireless-symbolic",
|
||||
// title: "Network",
|
||||
// contentType: network.Selection,
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "bluetooth",
|
||||
// icon: "bluetooth-symbolic",
|
||||
// title: "Bluetooth",
|
||||
// contentType: bluetooth.Devices,
|
||||
// }),
|
||||
// Box({
|
||||
// children: [darkmode.DarkToggle(), nightmode.NightToggle()],
|
||||
// }),
|
||||
// Box({
|
||||
// children: [audio.AppMixerToggle(), audio.MuteToggle()],
|
||||
// }),
|
||||
// Submenu({
|
||||
// menuName: "app-mixer",
|
||||
// icon: FontIcon({ icon: "" }),
|
||||
// title: "App Mixer",
|
||||
// contentType: audio.AppMixer,
|
||||
// }),
|
||||
Widget.Box({
|
||||
children: mprisService
|
||||
.bind("players")
|
||||
.as((x) => x.map((player) => mpris.MprisPlayer({ player }))),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -2,16 +2,6 @@ import App from 'resource:///com/github/Aylur/ags/app.js'
|
||||
import { Box, Label, Revealer, EventBox, Overlay, Icon } from 'resource:///com/github/Aylur/ags/widget.js'
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
export const addElipsis = (str, max = 20, position = "end") => {
|
||||
if (str.length <= max) return str;
|
||||
switch (position) {
|
||||
case "middle":
|
||||
max -= 3;
|
||||
return str.substring(0, Math.ceil(max / 2)) + "..." + str.substring(str.length - Math.floor(max / 2));
|
||||
case "end":
|
||||
return str.substring(0, max - 3) + "...";
|
||||
}
|
||||
};
|
||||
|
||||
export const Spinner = ({ icon = "process-working-symbolic" } = {}) =>
|
||||
Icon({
|
||||
@@ -125,70 +115,3 @@ export const Separator = ({ className = "", ...props } = {}) =>
|
||||
...props,
|
||||
className: `${className} separator accent`,
|
||||
});
|
||||
|
||||
const PopupCloser = (windowName) =>
|
||||
EventBox({
|
||||
hexpand: true,
|
||||
vexpand: true,
|
||||
connections: [["button-press-event", () => App.toggleWindow(windowName)]],
|
||||
});
|
||||
|
||||
const PopupRevealer = (windowName, transition, child) =>
|
||||
Box({
|
||||
css: "padding: 1px;",
|
||||
children: [
|
||||
Revealer({
|
||||
transition,
|
||||
child,
|
||||
transitionDuration: 350,
|
||||
connections: [
|
||||
[
|
||||
App,
|
||||
(revealer, name, visible) => {
|
||||
if (name === windowName) revealer.reveal_child = visible;
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
export const PopupOverlay = (windowName, layout, child) => {
|
||||
switch (layout) {
|
||||
case "top":
|
||||
return Box({
|
||||
children: [
|
||||
PopupCloser(windowName),
|
||||
Box({
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
children: [PopupRevealer(windowName, "slide_down", child), PopupCloser(windowName)],
|
||||
}),
|
||||
PopupCloser(windowName),
|
||||
],
|
||||
});
|
||||
case "top right":
|
||||
return Box({
|
||||
children: [
|
||||
PopupCloser(windowName),
|
||||
Box({
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
children: [PopupRevealer(windowName, "slide_down", child), PopupCloser(windowName)],
|
||||
}),
|
||||
],
|
||||
});
|
||||
case "center":
|
||||
return Box({
|
||||
children: [
|
||||
PopupCloser(windowName),
|
||||
Box({
|
||||
hexpand: false,
|
||||
vertical: true,
|
||||
children: [PopupCloser(windowName), PopupRevealer(windowName, "crossfade", child), PopupCloser(windowName)],
|
||||
}),
|
||||
PopupCloser(windowName),
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import Gtk from "gi://Gtk?version=3.0";
|
||||
|
||||
export const opened = Variable("");
|
||||
App.connect("window-toggled", (_, name, visible) => {
|
||||
if (name === "quicksettings" && !visible)
|
||||
Utils.timeout(500, () => (opened.value = ""));
|
||||
});
|
||||
|
||||
/**
|
||||
* @param {{
|
||||
* name: string,
|
||||
* activate?: false | (() => void),
|
||||
* } & import("types/widgets/button").ButtonProps} props
|
||||
*/
|
||||
export const Arrow = ({ name, activate, ...props }) => {
|
||||
let deg = 0;
|
||||
let iconOpened = false;
|
||||
const icon = Widget.Icon("pan-end-symbolic").hook(opened, () => {
|
||||
if (
|
||||
(opened.value === name && !iconOpened) ||
|
||||
(opened.value !== name && iconOpened)
|
||||
) {
|
||||
const step = opened.value === name ? 10 : -10;
|
||||
iconOpened = !iconOpened;
|
||||
for (let i = 0; i < 9; ++i) {
|
||||
Utils.timeout(15 * i, () => {
|
||||
deg += step;
|
||||
icon.setCss(`-gtk-icon-transform: rotate(${deg}deg);`);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return Widget.Button({
|
||||
child: icon,
|
||||
className: "qs-icon",
|
||||
onClicked: () => {
|
||||
opened.value = opened.value === name ? "" : name;
|
||||
if (typeof activate === "function") activate();
|
||||
},
|
||||
...props,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* name: string,
|
||||
* icon: string,
|
||||
* title: string,
|
||||
* content: Gtk.Widget[],
|
||||
* } & import("types/widgets/revealer").RevealerProps} MenuProps
|
||||
* @param {MenuProps} props
|
||||
*/
|
||||
export const Menu = ({ name, icon, title, content, ...props }) =>
|
||||
Widget.Revealer({
|
||||
transition: "slide_down",
|
||||
reveal_child: opened.bind().as((v) => v === name),
|
||||
child: Widget.Box({
|
||||
className: "qs-submenu surface",
|
||||
vertical: true,
|
||||
children: [
|
||||
Widget.Box({
|
||||
className: "qs-sub-title accent",
|
||||
children: [
|
||||
Widget.Icon({
|
||||
icon,
|
||||
}),
|
||||
Widget.Label({
|
||||
className: "bold f16",
|
||||
truncate: "end",
|
||||
label: title,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
Widget.Separator(),
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
className: "qs-sub-content",
|
||||
children: content,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
...props,
|
||||
});
|
||||
@@ -19,7 +19,7 @@ export const Padding = (
|
||||
vexpand,
|
||||
can_focus: false,
|
||||
child: Widget.Box({ css }),
|
||||
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
|
||||
setup: (w) => w.on("button-press-event", () => App.toggleWindow(name)),
|
||||
});
|
||||
/**
|
||||
* @param {string} name
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import GLib from "gi://GLib?version=2.0"
|
||||
|
||||
/**
|
||||
* @param {string | null | undefined} name
|
||||
* @param {string | null | undefined} fallback
|
||||
*/
|
||||
export function icon(name, fallback) {
|
||||
if (!name) return fallback || "";
|
||||
|
||||
if (GLib.file_test(name, GLib.FileTest.EXISTS)) return name;
|
||||
|
||||
const sub = substitutes[name];
|
||||
if (sub && Utils.lookUpIcon(sub)) return sub;
|
||||
if (Utils.lookUpIcon(name)) return name;
|
||||
return fallback || "";
|
||||
}
|
||||
|
||||
export const substitutes = {
|
||||
"transmission-gtk": "transmission",
|
||||
"blueberry.py": "blueberry",
|
||||
Caprine: "facebook-messenger",
|
||||
"com.raggesilver.BlackBox-symbolic": "terminal-symbolic",
|
||||
"org.wezfurlong.wezterm-symbolic": "terminal-symbolic",
|
||||
"audio-headset-bluetooth": "audio-headphones-symbolic",
|
||||
"audio-card-analog-usb": "audio-speakers-symbolic",
|
||||
"audio-card-analog-pci": "audio-card-symbolic",
|
||||
"preferences-system": "emblem-system-symbolic",
|
||||
"com.github.Aylur.ags-symbolic": "controls-symbolic",
|
||||
"com.github.Aylur.ags": "controls-symbolic",
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
// import { Separator, FontIcon, addElipsis } from "../misc.js";
|
||||
// import { ArrowToggle, opened } from "../services/quicksettings.js";
|
||||
//
|
||||
import { icon } from "../misc/utils.js";
|
||||
import { Arrow, Menu } from "../misc/menu.js";
|
||||
|
||||
const audio = await Service.import("audio");
|
||||
|
||||
const volumeIcons = /** @type {const} */ ([
|
||||
@@ -11,16 +11,18 @@ const volumeIcons = /** @type {const} */ ([
|
||||
[0, "muted"],
|
||||
]);
|
||||
|
||||
/** @param {import("types/widgets/icon").IconProps} props */
|
||||
export const VolumeIndicator = (props) =>
|
||||
Widget.Icon({
|
||||
icon: audio.speaker.bind("volume").as((vol) => {
|
||||
const val = volumeIcons.find(
|
||||
([threshold]) => threshold <= vol * 100,
|
||||
)?.[1];
|
||||
return `audio-volume-${val}-symbolic`;
|
||||
}),
|
||||
...props,
|
||||
/** @param {{type?: "speaker" | "microphone"} & import("types/widgets/icon").IconProps} props */
|
||||
export const VolumeIndicator = ({ type = "speaker", ...props }) =>
|
||||
Widget.Icon(props).hook(audio, (self) => {
|
||||
if (audio[type].is_muted) {
|
||||
self.icon = "audio-volume-muted-symbolic";
|
||||
self.tooltip_text = "Muted";
|
||||
return;
|
||||
}
|
||||
const vol = audio[type].volume * 100;
|
||||
const icon = volumeIcons.find(([threshold]) => threshold <= vol)?.[1];
|
||||
self.icon = `audio-volume-${icon}-symbolic`;
|
||||
self.tooltip_text = `Volume: ${Math.floor(vol)}%`;
|
||||
});
|
||||
|
||||
/** @param {import("types/widgets/icon").IconProps} props */
|
||||
@@ -45,23 +47,52 @@ export const MicrophoneIndicator = (props) =>
|
||||
// return item;
|
||||
// };
|
||||
//
|
||||
//
|
||||
// export const SpeakerPercentLabel = (props) =>
|
||||
// Label({
|
||||
// ...props,
|
||||
// connections: [
|
||||
// [
|
||||
// Audio,
|
||||
// (label) => {
|
||||
// if (!Audio.speaker) return;
|
||||
//
|
||||
// const perc = Math.floor(Audio.speaker.volume * 100);
|
||||
// label.label = `${(" " + perc).slice(-3)}%`;
|
||||
// },
|
||||
// "speaker-changed",
|
||||
// ],
|
||||
// ],
|
||||
// });
|
||||
|
||||
/** @param {{type?: "speaker" | "microphone"} & import("types/widgets/slider").SliderProps} props */
|
||||
const VolumeSlider = ({ type = "speaker", ...props }) =>
|
||||
Widget.Slider({
|
||||
hexpand: true,
|
||||
draw_value: false,
|
||||
onChange: ({ value, dragging }) => {
|
||||
if (dragging) {
|
||||
audio[type].volume = value;
|
||||
audio[type].is_muted = false;
|
||||
}
|
||||
},
|
||||
value: audio[type].bind("volume"),
|
||||
css: audio[type].bind("is_muted").as((x) => `opacity: ${x ? 0.7 : 1}`),
|
||||
...props,
|
||||
});
|
||||
|
||||
/** @param {{type?: "speaker" | "microphone"} & import("types/widgets/box").BoxProps} props */
|
||||
export const Volume = ({ type = "speaker", ...props }) =>
|
||||
Widget.Box({
|
||||
className: "qs-slider",
|
||||
children: [
|
||||
Widget.Button({
|
||||
onClicked: () => {
|
||||
audio[type].is_muted = !audio[type].is_muted;
|
||||
},
|
||||
child: VolumeIndicator({ type }),
|
||||
}),
|
||||
VolumeSlider({ type }),
|
||||
Widget.Label({
|
||||
label: audio[type]
|
||||
.bind("volume")
|
||||
.as((vol) => `${Math.floor(vol * 100)}%`),
|
||||
}),
|
||||
Widget.Box({
|
||||
vpack: "center",
|
||||
child: Arrow({ name: "sink-selector" }),
|
||||
}),
|
||||
Widget.Box({
|
||||
vpack: "center",
|
||||
child: Arrow({ name: "app-mixer" }),
|
||||
visible: audio.bind("apps").as((a) => a.length > 0),
|
||||
}),
|
||||
],
|
||||
...props,
|
||||
});
|
||||
//
|
||||
// export const SpeakerSlider = (props) => {
|
||||
// const slider = Slider({
|
||||
@@ -87,7 +118,7 @@ export const MicrophoneIndicator = (props) =>
|
||||
// slider.max = 1.5;
|
||||
// return slider;
|
||||
// };
|
||||
//
|
||||
|
||||
// export const MuteToggle = (props) =>
|
||||
// Button({
|
||||
// ...props,
|
||||
@@ -192,61 +223,45 @@ export const MicrophoneIndicator = (props) =>
|
||||
// ],
|
||||
// });
|
||||
// };
|
||||
//
|
||||
// export const StreamSelector = ({ streams = "speakers", ...props } = {}) =>
|
||||
// Box({
|
||||
// ...props,
|
||||
// vertical: true,
|
||||
// connections: [
|
||||
// [
|
||||
// Audio,
|
||||
// (box) => {
|
||||
// box.children = Audio[streams]
|
||||
// .map((stream) =>
|
||||
// Button({
|
||||
// child: Box({
|
||||
// children: [
|
||||
// Icon({
|
||||
// icon: iconSubstitute(stream.iconName),
|
||||
// tooltipText: stream.iconName,
|
||||
// }),
|
||||
// Label(stream.description.split(" ").slice(0, 4).join(" ")),
|
||||
// Icon({
|
||||
// icon: "object-select-symbolic",
|
||||
// hexpand: true,
|
||||
// hpack: "end",
|
||||
// connections: [
|
||||
// [
|
||||
// "draw",
|
||||
// (icon) => {
|
||||
// icon.visible = Audio.speaker === stream;
|
||||
// },
|
||||
// ],
|
||||
// ],
|
||||
// }),
|
||||
// ],
|
||||
// }),
|
||||
// onClicked: () => {
|
||||
// if (streams === "speakers") Audio.speaker = stream;
|
||||
//
|
||||
// if (streams === "microphones") Audio.microphone = stream;
|
||||
// },
|
||||
// }),
|
||||
// )
|
||||
// .concat([
|
||||
// Separator(),
|
||||
// Button({
|
||||
// onClicked: () => {
|
||||
// execAsync("pavucontrol").catch(print);
|
||||
// App.closeWindow("quicksettings");
|
||||
// },
|
||||
// child: Label({
|
||||
// label: "Settings",
|
||||
// xalign: 0,
|
||||
// }),
|
||||
// }),
|
||||
// ]);
|
||||
// },
|
||||
// ],
|
||||
// ],
|
||||
// });
|
||||
|
||||
/** @param {Partial<import("../misc/menu.js").MenuProps>} props */
|
||||
export const SinkSelector = (props) =>
|
||||
Menu({
|
||||
name: "sink-selector",
|
||||
icon: "audio-headphones-symbolic",
|
||||
title: "Sink Selector",
|
||||
content: [
|
||||
Widget.Box({
|
||||
vertical: true,
|
||||
children: audio.bind("speakers").as((a) => a.map(SinkItem)),
|
||||
}),
|
||||
Widget.Separator(),
|
||||
// SettingsButton(),
|
||||
],
|
||||
...props,
|
||||
});
|
||||
|
||||
/** @param {import("types/service/audio").Stream} stream */
|
||||
const SinkItem = (stream) =>
|
||||
Widget.Button({
|
||||
hexpand: true,
|
||||
onClicked: () => (audio.speaker = stream),
|
||||
child: Widget.Box({
|
||||
css: "margin-top: 6px; margin-bottom: 6px;",
|
||||
children: [
|
||||
Widget.Icon({
|
||||
icon: icon(stream.icon_name, "audio-x-generic-symbolic"),
|
||||
tooltip_text: stream.icon_name || "",
|
||||
}),
|
||||
Widget.Label({
|
||||
label: (stream.description || "").split(" ").slice(0, 4).join(" "),
|
||||
}),
|
||||
Widget.Icon({
|
||||
icon: "object-select-symbolic",
|
||||
hexpand: true,
|
||||
hpack: "end",
|
||||
visible: audio.speaker.bind("stream").as((s) => s === stream.stream),
|
||||
}),
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1,15 +1,3 @@
|
||||
import App from 'resource:///com/github/Aylur/ags/app.js'
|
||||
import Service from 'resource:///com/github/Aylur/ags/service.js'
|
||||
import Variable from 'resource:///com/github/Aylur/ags/variable.js';
|
||||
import { Box, Button, Icon } from 'resource:///com/github/Aylur/ags/widget.js'
|
||||
import { timeout } from 'resource:///com/github/Aylur/ags/utils.js';
|
||||
|
||||
export const opened = Variable('');
|
||||
App.connect('window-toggled', (_, name, visible) => {
|
||||
if (name === 'quicksettings' && !visible)
|
||||
timeout(500, () => opened.value = '');
|
||||
});
|
||||
|
||||
export const ArrowToggle = ({ icon, label, toggle, name, expand, ...props }) => {
|
||||
icon.className = `${icon.className} qs-icon`;
|
||||
return Box({
|
||||
@@ -27,45 +15,3 @@ export const ArrowToggle = ({ icon, label, toggle, name, expand, ...props }) =>
|
||||
...props,
|
||||
});
|
||||
};
|
||||
|
||||
export const Arrow = ({ name, expand, ...props }) =>
|
||||
Button({
|
||||
...props,
|
||||
className: "qs-icon",
|
||||
onClicked: () => {
|
||||
opened.value = opened.value === name ? "" : name;
|
||||
if (expand) expand();
|
||||
},
|
||||
connections: [
|
||||
[
|
||||
opened,
|
||||
(button) => {
|
||||
button.toggleClassName("opened", opened.value === name);
|
||||
},
|
||||
],
|
||||
],
|
||||
child: Icon({
|
||||
icon: "pan-end-symbolic",
|
||||
properties: [
|
||||
["deg", 0],
|
||||
["opened", false],
|
||||
],
|
||||
connections: [
|
||||
[
|
||||
opened,
|
||||
(icon) => {
|
||||
if ((opened.value === name && !icon._opened) || (opened.value !== name && icon._opened)) {
|
||||
const step = opened.value === name ? 10 : -10;
|
||||
icon._opened = !icon._opened;
|
||||
for (let i = 0; i < 9; ++i) {
|
||||
timeout(5 * i, () => {
|
||||
icon._deg += step;
|
||||
icon.setCss(`-gtk-icon-transform: rotate(${icon._deg}deg);`);
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
],
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user