feat: settings button in quick toggles now animate from util panel
This commit is contained in:
@@ -10,7 +10,6 @@ CustomFlickable {
|
|||||||
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
property real bottomFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||||
property real fadeAmount: 0.1
|
property real fadeAmount: 0.1
|
||||||
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
property real topFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||||
readonly property real dpr: Math.ceil((QsWindow.window as QsWindow)?.devicePixelRatio ?? 1)
|
|
||||||
|
|
||||||
function fadeShouldBeActive(isStart: bool): bool {
|
function fadeShouldBeActive(isStart: bool): bool {
|
||||||
// When content is smaller than flickable size, hide fade when rebound starts
|
// When content is smaller than flickable size, hide fade when rebound starts
|
||||||
@@ -30,7 +29,6 @@ CustomFlickable {
|
|||||||
type: Anim.SlowEffects
|
type: Anim.SlowEffects
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
layer.textureSize: Qt.size(width * dpr, height * dpr)
|
|
||||||
layer.effect: Mask {
|
layer.effect: Mask {
|
||||||
maskSource: mask
|
maskSource: mask
|
||||||
|
|
||||||
|
|||||||
@@ -6,11 +6,12 @@ import qs.Effects
|
|||||||
CustomListView {
|
CustomListView {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property bool horizontal: orientation === ListView.Horizontal
|
||||||
|
|
||||||
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
property real endFadeOpacity: fadeShouldBeActive(false) ? 0 : 1
|
||||||
property bool fadeEnabled: true
|
property bool fadeEnabled: true
|
||||||
property real fadeAmount: 0.1
|
property real fadeAmount: 0.1
|
||||||
property real fadeThreshold: 0.0
|
property real fadeThreshold: 0.0
|
||||||
readonly property bool horizontal: orientation === ListView.Horizontal
|
|
||||||
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
property real startFadeOpacity: fadeShouldBeActive(true) ? 0 : 1
|
||||||
|
|
||||||
function contentSize(): real {
|
function contentSize(): real {
|
||||||
|
|||||||
+1
-2
@@ -145,9 +145,8 @@ Item {
|
|||||||
Utils.Wrapper {
|
Utils.Wrapper {
|
||||||
id: utilities
|
id: utilities
|
||||||
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
anchors.right: parent.right
|
|
||||||
screenState: root.screenState
|
screenState: root.screenState
|
||||||
|
screen: root.screen
|
||||||
sidebar: sidebar
|
sidebar: sidebar
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,7 +50,6 @@ Region {
|
|||||||
R {
|
R {
|
||||||
height: panel.height * (1 - root.panels.utilities.offsetScale) + root.geometry.insetBottom(root.borderThickness)
|
height: panel.height * (1 - root.panels.utilities.offsetScale) + root.geometry.insetBottom(root.borderThickness)
|
||||||
panel: root.panels.utilities
|
panel: root.panels.utilities
|
||||||
y: root.win.height - height
|
|
||||||
}
|
}
|
||||||
|
|
||||||
R {
|
R {
|
||||||
|
|||||||
+1
-1
@@ -266,7 +266,7 @@ CustomWindow {
|
|||||||
deformAmount: 0.1
|
deformAmount: 0.1
|
||||||
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
|
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
|
||||||
panel: panels.utilities
|
panel: panels.utilities
|
||||||
topLeftRadius: 0
|
radius: panels.utilities.isDetached ? Tokens.rounding.largeIncreased + Tokens.padding.small : 0
|
||||||
}
|
}
|
||||||
|
|
||||||
PanelBg {
|
PanelBg {
|
||||||
|
|||||||
@@ -2,16 +2,15 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import QtQuick.Layouts
|
import QtQuick.Layouts
|
||||||
import Quickshell
|
|
||||||
import Quickshell.Bluetooth
|
import Quickshell.Bluetooth
|
||||||
import Quickshell.Networking as QSNetwork
|
import Quickshell.Networking as QSNetwork
|
||||||
import ZShell.Components
|
import ZShell.Components
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import qs.Modules.Sidebar.Utils
|
||||||
import qs.Components
|
import qs.Components
|
||||||
import qs.Services
|
import qs.Services
|
||||||
import qs.Helpers
|
import qs.Helpers
|
||||||
import qs.Daemons
|
import qs.Daemons
|
||||||
import qs.Modules.Settings
|
|
||||||
|
|
||||||
CustomRect {
|
CustomRect {
|
||||||
id: root
|
id: root
|
||||||
@@ -38,16 +37,18 @@ CustomRect {
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
required property ScreenState screenState
|
|
||||||
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
|
readonly property int splitIndex: Math.ceil(quickToggles.length / 2)
|
||||||
|
|
||||||
|
required property PopoutState popouts
|
||||||
|
required property ScreenState screenState
|
||||||
|
|
||||||
color: Colors.tPalette.m3surfaceContainer
|
color: Colors.tPalette.m3surfaceContainer
|
||||||
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
|
implicitHeight: layout.implicitHeight + Tokens.padding.extraLargeIncreased
|
||||||
radius: Tokens.rounding.small
|
radius: Tokens.rounding.small
|
||||||
|
|
||||||
SnapLayout {
|
SnapLayout {
|
||||||
orientation: Qt.Vertical
|
|
||||||
id: layout
|
id: layout
|
||||||
|
orientation: Qt.Vertical
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: Tokens.padding.large
|
anchors.margins: Tokens.padding.large
|
||||||
@@ -167,8 +168,7 @@ orientation: Qt.Vertical
|
|||||||
isToggle: false
|
isToggle: false
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
root.screenState.sidebar = false;
|
root.popouts.detachRequested("appearance");
|
||||||
root.screenState.settings = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ import ZShell.Components
|
|||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
|
readonly property int padding: Tokens.padding.small
|
||||||
|
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
|
required property PopoutState popouts
|
||||||
|
|
||||||
implicitHeight: ZUtils.snap(layout.implicitHeight, root)
|
implicitHeight: ZUtils.snap(layout.implicitHeight, root)
|
||||||
implicitWidth: ZUtils.snap(layout.implicitWidth, root)
|
implicitWidth: ZUtils.snap(layout.implicitWidth, root)
|
||||||
@@ -33,6 +36,7 @@ Item {
|
|||||||
Toggles {
|
Toggles {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
screenState: root.screenState
|
screenState: root.screenState
|
||||||
|
popouts: root.popouts
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property string currentName
|
||||||
|
property string hasCurrent
|
||||||
|
|
||||||
|
signal detachRequested(mode: string)
|
||||||
|
}
|
||||||
@@ -2,41 +2,373 @@ pragma ComponentBehavior: Bound
|
|||||||
|
|
||||||
import QtQuick
|
import QtQuick
|
||||||
import Quickshell
|
import Quickshell
|
||||||
|
import Quickshell.Hyprland
|
||||||
|
import Quickshell.Wayland
|
||||||
import ZShell.Config
|
import ZShell.Config
|
||||||
|
import ZShell
|
||||||
|
import qs.Modules.Settings as Settings
|
||||||
import qs.Components
|
import qs.Components
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property real offsetScale: shouldBeActive ? 0 : 1
|
|
||||||
required property ScreenState screenState
|
required property ScreenState screenState
|
||||||
readonly property bool shouldBeActive: screenState.sidebar && !sidebar.chatActive
|
required property ShellScreen screen
|
||||||
required property Item sidebar
|
required property Item sidebar
|
||||||
|
|
||||||
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
|
property string queuedMode
|
||||||
implicitHeight: content.implicitHeight + Tokens.padding.small * 2
|
property real offsetScale: shouldBeActive ? 0 : 1
|
||||||
implicitWidth: sidebar.width * (1 - sidebar.offsetScale)
|
|
||||||
|
readonly property string wantedState: {
|
||||||
|
if (queuedMode.length > 0)
|
||||||
|
return "detached";
|
||||||
|
if (screenState.sidebar && !sidebar.chatActive)
|
||||||
|
return "docked";
|
||||||
|
return "hidden";
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property bool isDetached: wantedState === "detached"
|
||||||
|
readonly property bool shouldBeActive: wantedState !== "hidden"
|
||||||
|
|
||||||
|
readonly property int dur: Tokens.anim.durations.expressiveDefaultSpatial
|
||||||
|
readonly property int slowDur: Tokens.anim.durations.expressiveSlowSpatial
|
||||||
|
|
||||||
|
// Sizes come from the specific Comp, never from "whichever child is current"
|
||||||
|
readonly property real settingsW: settings.implicitWidth + (settings.item?.padding ?? 0) * 2
|
||||||
|
readonly property real settingsH: settings.implicitHeight + (settings.item?.padding ?? 0) * 2
|
||||||
|
readonly property real contentW: content.implicitWidth + (content.item?.padding ?? 0) * 2
|
||||||
|
readonly property real contentH: content.implicitHeight + (content.item?.padding ?? 0) * 2
|
||||||
|
|
||||||
|
// Slot positions
|
||||||
|
readonly property real hiddenX: parent.width
|
||||||
|
readonly property real hiddenY: parent.height - Config.bar.border + Tokens.padding.small * 2
|
||||||
|
readonly property real dockedX: parent.width - sidebar.width
|
||||||
|
readonly property real dockedY: parent.height - contentH
|
||||||
|
readonly property real centerX: (parent.screen.width - settingsW) / 2
|
||||||
|
readonly property real centerY: (parent.screen.height - settingsH) / 2
|
||||||
|
|
||||||
opacity: 1 - offsetScale
|
opacity: 1 - offsetScale
|
||||||
visible: offsetScale < 1
|
visible: offsetScale < 1
|
||||||
|
clip: true
|
||||||
|
|
||||||
|
function detach(mode: string): void {
|
||||||
|
queuedMode = mode;
|
||||||
|
focus = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function close(): void {
|
||||||
|
queuedMode = "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- State selection --------------------------------------------------
|
||||||
|
// `state` is only ever assigned `wantedState`, never an intermediate value.
|
||||||
|
// The only thing sync() decides is WHEN, i.e. whether the Comp the target
|
||||||
|
// state takes its size from is Ready yet.
|
||||||
|
// Restarted whenever either Comp's status or size changes. While it is
|
||||||
|
// running, a Comp's implicit size is still settling and must not be captured.
|
||||||
|
Timer {
|
||||||
|
id: settle
|
||||||
|
|
||||||
|
interval: 12 // about two frames; raise it if the height is still read too early
|
||||||
|
onTriggered: root.sync()
|
||||||
|
}
|
||||||
|
|
||||||
|
function stable(comp: Comp): bool {
|
||||||
|
return comp.status === Loader.Ready && comp.implicitHeight > 0 && !settle.running;
|
||||||
|
}
|
||||||
|
|
||||||
|
function ready(): bool {
|
||||||
|
switch (wantedState) {
|
||||||
|
case "detached":
|
||||||
|
return stable(settings);
|
||||||
|
case "docked":
|
||||||
|
return stable(content);
|
||||||
|
default:
|
||||||
|
// Closing from detached passes through the docked slot, so
|
||||||
|
// content's final size is needed before the transition starts.
|
||||||
|
return state !== "detached" || stable(content);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sync(): void {
|
||||||
|
if (state !== wantedState && ready())
|
||||||
|
state = wantedState;
|
||||||
|
}
|
||||||
|
|
||||||
|
onWantedStateChanged: sync()
|
||||||
|
Component.onCompleted: sync()
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: settings
|
||||||
|
function onStatusChanged(): void {
|
||||||
|
settle.restart();
|
||||||
|
}
|
||||||
|
function onImplicitHeightChanged(): void {
|
||||||
|
settle.restart();
|
||||||
|
}
|
||||||
|
function onImplicitWidthChanged(): void {
|
||||||
|
settle.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: content
|
||||||
|
function onStatusChanged(): void {
|
||||||
|
settle.restart();
|
||||||
|
}
|
||||||
|
function onImplicitHeightChanged(): void {
|
||||||
|
settle.restart();
|
||||||
|
}
|
||||||
|
function onImplicitWidthChanged(): void {
|
||||||
|
settle.restart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- States: every position and size property lives here ------------
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "hidden"
|
||||||
|
PropertyChanges {
|
||||||
|
root.x: root.hiddenX
|
||||||
|
root.y: root.hiddenY
|
||||||
|
root.implicitWidth: 0
|
||||||
|
root.implicitHeight: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "docked"
|
||||||
|
PropertyChanges {
|
||||||
|
root.x: root.dockedX
|
||||||
|
root.y: root.dockedY
|
||||||
|
root.implicitWidth: root.contentW
|
||||||
|
root.implicitHeight: root.contentH
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "detached"
|
||||||
|
PropertyChanges {
|
||||||
|
root.x: root.centerX
|
||||||
|
root.y: root.centerY
|
||||||
|
root.implicitWidth: root.settingsW
|
||||||
|
root.implicitHeight: root.settingsH
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// One explicit transition per state pair. No `reversible`, no wildcards.
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
from: "hidden"
|
||||||
|
to: "docked"
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
properties: "x,y,implicitWidth,implicitHeight"
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "docked"
|
||||||
|
to: "hidden"
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
properties: "x,y,implicitWidth,implicitHeight"
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "docked"
|
||||||
|
to: "detached"
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
properties: "x,y,implicitWidth,implicitHeight"
|
||||||
|
duration: root.slowDur
|
||||||
|
easing: Tokens.anim.expressiveSlowSpatial
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "hidden"
|
||||||
|
to: "detached"
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
properties: "x,y,implicitWidth,implicitHeight"
|
||||||
|
duration: root.slowDur
|
||||||
|
easing: Tokens.anim.expressiveSlowSpatial
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "detached"
|
||||||
|
to: "docked"
|
||||||
|
|
||||||
|
Anim {
|
||||||
|
properties: "x,y,implicitWidth,implicitHeight"
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
// 1) x, y and size to the docked slot, 2) only y drops, 3) snap the rest while invisible
|
||||||
|
Transition {
|
||||||
|
from: "detached"
|
||||||
|
to: "hidden"
|
||||||
|
|
||||||
|
ParallelAnimation {
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "x"
|
||||||
|
to: root.dockedX
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "y"
|
||||||
|
to: root.dockedY
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "implicitWidth"
|
||||||
|
to: root.contentW
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "implicitHeight"
|
||||||
|
to: root.contentH
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
PauseAnimation {
|
||||||
|
duration: root.dur - 300
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
target: root
|
||||||
|
property: "y"
|
||||||
|
from: root.dockedY
|
||||||
|
to: root.hiddenY
|
||||||
|
duration: root.dur
|
||||||
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
target: root
|
||||||
|
properties: "x,implicitWidth,implicitHeight"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
// Fade only. Nothing that moves or sizes the item is a Behavior any more.
|
||||||
Behavior on offsetScale {
|
Behavior on offsetScale {
|
||||||
Anim {
|
Anim {
|
||||||
duration: Tokens.anim.durations.expressiveDefaultSpatial
|
duration: root.dur
|
||||||
easing: Tokens.anim.expressiveDefaultSpatial
|
easing: Tokens.anim.expressiveDefaultSpatial
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Loader {
|
PopoutState {
|
||||||
|
id: popoutState
|
||||||
|
|
||||||
|
onDetachRequested: mode => root.detach(mode)
|
||||||
|
}
|
||||||
|
|
||||||
|
HyprlandFocusGrab {
|
||||||
|
active: root.isDetached
|
||||||
|
windows: [QsWindow.window]
|
||||||
|
onCleared: root.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
Binding {
|
||||||
|
when: root.isDetached
|
||||||
|
|
||||||
|
target: QsWindow.window
|
||||||
|
property: "WlrLayershell.keyboardFocus"
|
||||||
|
value: WlrKeyboardFocus.OnDemand
|
||||||
|
}
|
||||||
|
|
||||||
|
Comp {
|
||||||
|
id: settings
|
||||||
|
|
||||||
|
readonly property int padding: 0
|
||||||
|
|
||||||
|
shouldBeActive: root.isDetached
|
||||||
|
anchors.centerIn: parent
|
||||||
|
|
||||||
|
sourceComponent: Settings.Content {
|
||||||
|
sState.screen: root.screen
|
||||||
|
sState.animatingContainer: settings.opacity < 1
|
||||||
|
sState.currentPageIdx: ["appearance", "network", "bluetooth", "audio"].indexOf(root.queuedMode)
|
||||||
|
onClose: root.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Comp {
|
||||||
id: content
|
id: content
|
||||||
|
|
||||||
active: root.shouldBeActive || root.visible
|
shouldBeActive: (root.shouldBeActive || root.visible) && !root.isDetached
|
||||||
anchors.left: parent.left
|
anchors.centerIn: parent
|
||||||
anchors.margins: 8
|
|
||||||
anchors.top: parent.top
|
|
||||||
|
|
||||||
sourceComponent: Content {
|
sourceComponent: Content {
|
||||||
implicitWidth: root.implicitWidth - 8 * 2
|
popouts: popoutState
|
||||||
|
implicitWidth: root.sidebar.width - Tokens.padding.small * 2
|
||||||
screenState: root.screenState
|
screenState: root.screenState
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
component Comp: Loader {
|
||||||
|
id: comp
|
||||||
|
|
||||||
|
property bool shouldBeActive
|
||||||
|
|
||||||
|
active: false
|
||||||
|
opacity: 0
|
||||||
|
|
||||||
|
// Makes the loader load on the same frame shouldBeActive becomes true, which ensures size is set
|
||||||
|
states: State {
|
||||||
|
name: "active"
|
||||||
|
when: comp.shouldBeActive
|
||||||
|
|
||||||
|
PropertyChanges {
|
||||||
|
comp.opacity: 1
|
||||||
|
comp.active: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
transitions: [
|
||||||
|
Transition {
|
||||||
|
from: ""
|
||||||
|
to: "active"
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
PropertyAction {
|
||||||
|
property: "active"
|
||||||
|
}
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
property: "opacity"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
Transition {
|
||||||
|
from: "active"
|
||||||
|
to: ""
|
||||||
|
|
||||||
|
SequentialAnimation {
|
||||||
|
Anim {
|
||||||
|
type: Anim.DefaultEffects
|
||||||
|
property: "opacity"
|
||||||
|
}
|
||||||
|
PropertyAction {
|
||||||
|
property: "active"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user