fix(qbz-qt): make listen list reordering tangible
cx
This commit is contained in:
5 files changed
+141
-14
No files matched your search
@@ -124,6 +124,10 @@ Rectangle {
|
|||||||
/// and consumes the shared drag coordinates; all existing rows inherit
|
/// and consumes the shared drag coordinates; all existing rows inherit
|
||||||
/// `showReorder` and therefore keep their current gesture policy.
|
/// `showReorder` and therefore keep their current gesture policy.
|
||||||
property bool reorderDrag: root.showReorder
|
property bool reorderDrag: root.showReorder
|
||||||
|
/// QueueView replaces the compact window-level drag pill with a complete
|
||||||
|
/// TrackRow while the pointer remains over its list. Other surfaces keep
|
||||||
|
/// the portable pill because their target may live outside the view.
|
||||||
|
property bool inlineDragVisual: false
|
||||||
/// Ends of the list: the reference dims nothing, but a chevron that
|
/// Ends of the list: the reference dims nothing, but a chevron that
|
||||||
/// renders and no-ops is the defect class this round is fixing, so the
|
/// renders and no-ops is the defect class this round is fixing, so the
|
||||||
/// first row's ↑ and the last row's ↓ are drawn disabled instead.
|
/// first row's ↑ and the last row's ↓ are drawn disabled instead.
|
||||||
@@ -1535,7 +1539,8 @@ Rectangle {
|
|||||||
root.dragging = true
|
root.dragging = true
|
||||||
root.bodyDragStarted(root.number)
|
root.bodyDragStarted(root.number)
|
||||||
QbzShell.dragStart(root.item.id, root.item.title || "",
|
QbzShell.dragStart(root.item.id, root.item.title || "",
|
||||||
(root.item.artist || "") + " · " + (root.item.album || ""), g.x, g.y)
|
(root.item.artist || "") + " · " + (root.item.album || ""),
|
||||||
|
g.x, g.y, root.inlineDragVisual)
|
||||||
}
|
}
|
||||||
if (root.dragging) QbzShell.dragMove(g.x, g.y)
|
if (root.dragging) QbzShell.dragMove(g.x, g.y)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -638,7 +638,7 @@ Rectangle {
|
|||||||
// the sidebar: "N tracks" for a group, or title + "artist · album" for
|
// the sidebar: "N tracks" for a group, or title + "artist · album" for
|
||||||
// one. Visual only (never blocks the pointer).
|
// one. Visual only (never blocks the pointer).
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: QbzShell.dragActive
|
visible: QbzShell.dragActive && !QbzShell.dragInlineVisual
|
||||||
x: QbzShell.dragX + 10
|
x: QbzShell.dragX + 10
|
||||||
y: QbzShell.dragY + 14
|
y: QbzShell.dragY + 14
|
||||||
width: ghostCol.width + 28
|
width: ghostCol.width + 28
|
||||||
|
|||||||
@@ -39,7 +39,10 @@ Rectangle {
|
|||||||
QbzQueue.queueExtendedOpened()
|
QbzQueue.queueExtendedOpened()
|
||||||
root.scheduleVisibleCovers()
|
root.scheduleVisibleCovers()
|
||||||
}
|
}
|
||||||
Component.onDestruction: QbzQueue.queueExtendedClosed()
|
Component.onDestruction: {
|
||||||
|
QbzShell.dragInlineVisual = false
|
||||||
|
QbzQueue.queueExtendedClosed()
|
||||||
|
}
|
||||||
onDocChanged: root.scheduleVisibleCovers()
|
onDocChanged: root.scheduleVisibleCovers()
|
||||||
|
|
||||||
// ----------------------------- artwork ------------------------------
|
// ----------------------------- artwork ------------------------------
|
||||||
@@ -205,36 +208,63 @@ Rectangle {
|
|||||||
QbzPlaylistPicker.openForTrack(row.id)
|
QbzPlaylistPicker.openForTrack(row.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The chevrons and the pointer drop share the same queue-wide insertion
|
||||||
|
// contract. Moving down inserts after the following row (`from + 2`),
|
||||||
|
// because `queueExtendedDrop` consumes a SLOT rather than a destination
|
||||||
|
// row index.
|
||||||
|
function moveQueueRow(row, delta) {
|
||||||
|
if (!row || row.phase !== "upcoming" || root.searchActive
|
||||||
|
|| root.rowBlocked(row))
|
||||||
|
return
|
||||||
|
var from = row.phaseIndex
|
||||||
|
var count = root.doc.upcomingCount || 0
|
||||||
|
if ((delta < 0 && from <= 0) || (delta > 0 && from >= count - 1))
|
||||||
|
return
|
||||||
|
var slot = delta < 0 ? from - 1 : from + 2
|
||||||
|
QbzQueue.queueExtendedDrop("upcoming", from, row.id, slot)
|
||||||
|
}
|
||||||
|
|
||||||
// -------------------------- local reorder ----------------------------
|
// -------------------------- local reorder ----------------------------
|
||||||
|
|
||||||
property string dragPhase: ""
|
property string dragPhase: ""
|
||||||
property int dragPhaseIndex: -1
|
property int dragPhaseIndex: -1
|
||||||
property string dragTrackId: ""
|
property string dragTrackId: ""
|
||||||
|
property var dragRow: null
|
||||||
|
property int dragRowNumber: 0
|
||||||
property int dropSlot: -1
|
property int dropSlot: -1
|
||||||
property real dropLineY: -1
|
property real dropLineY: -1
|
||||||
property bool dropHot: false
|
property bool dropHot: false
|
||||||
|
readonly property var dragDisplayItem: root.dragRow
|
||||||
|
? Object.assign({}, root.dragRow, { "artPath": root.coverPath(root.dragRow) })
|
||||||
|
: ({})
|
||||||
|
|
||||||
function beginQueueDrag(row) {
|
function beginQueueDrag(row, visualIndex) {
|
||||||
root.dragPhase = row.phase
|
root.dragPhase = row.phase
|
||||||
root.dragPhaseIndex = row.phaseIndex
|
root.dragPhaseIndex = row.phaseIndex
|
||||||
root.dragTrackId = row.id
|
root.dragTrackId = row.id
|
||||||
|
root.dragRow = row
|
||||||
|
root.dragRowNumber = visualIndex + 1
|
||||||
root.dropSlot = -1
|
root.dropSlot = -1
|
||||||
root.dropHot = false
|
root.dropHot = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearInlineDrop() {
|
||||||
|
root.dropHot = false
|
||||||
|
root.dropSlot = -1
|
||||||
|
QbzShell.dragInlineVisual = false
|
||||||
|
}
|
||||||
|
|
||||||
function recomputeDrop() {
|
function recomputeDrop() {
|
||||||
if (!QbzShell.dragActive || root.dragTrackId === "" || root.searchActive) {
|
if (!QbzShell.dragActive || root.dragTrackId === "" || root.searchActive) {
|
||||||
root.dropHot = false
|
root.clearInlineDrop()
|
||||||
root.dropSlot = -1
|
|
||||||
return
|
|
||||||
}
|
|
||||||
var p = listHost.mapFromItem(null, QbzShell.dragX, QbzShell.dragY)
|
|
||||||
if (p.x < 0 || p.x > queueList.width || p.y < 0 || p.y > listHost.height) {
|
|
||||||
root.dropHot = false
|
|
||||||
root.dropSlot = -1
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
var contentPoint = queueList.mapFromItem(null, QbzShell.dragX, QbzShell.dragY)
|
var contentPoint = queueList.mapFromItem(null, QbzShell.dragX, QbzShell.dragY)
|
||||||
|
if (contentPoint.x < 0 || contentPoint.x > queueList.width
|
||||||
|
|| contentPoint.y < 0 || contentPoint.y > queueList.height) {
|
||||||
|
root.clearInlineDrop()
|
||||||
|
return
|
||||||
|
}
|
||||||
var cy = queueList.contentY + contentPoint.y
|
var cy = queueList.contentY + contentPoint.y
|
||||||
var index = queueList.indexAt(Math.max(1, queueList.width / 2), cy)
|
var index = queueList.indexAt(Math.max(1, queueList.width / 2), cy)
|
||||||
var insertion
|
var insertion
|
||||||
@@ -248,6 +278,7 @@ Rectangle {
|
|||||||
root.dropSlot = Math.max(0, Math.min(root.doc.upcomingCount || 0,
|
root.dropSlot = Math.max(0, Math.min(root.doc.upcomingCount || 0,
|
||||||
insertion - root.firstUpcoming))
|
insertion - root.firstUpcoming))
|
||||||
root.dropHot = true
|
root.dropHot = true
|
||||||
|
QbzShell.dragInlineVisual = true
|
||||||
|
|
||||||
var fullIndex = root.firstUpcoming + root.dropSlot
|
var fullIndex = root.firstUpcoming + root.dropSlot
|
||||||
var lineItem = fullIndex < root.rows.length ? queueList.itemAtIndex(fullIndex) : null
|
var lineItem = fullIndex < root.rows.length ? queueList.itemAtIndex(fullIndex) : null
|
||||||
@@ -270,12 +301,21 @@ Rectangle {
|
|||||||
root.dragPhase = ""
|
root.dragPhase = ""
|
||||||
root.dragPhaseIndex = -1
|
root.dragPhaseIndex = -1
|
||||||
root.dragTrackId = ""
|
root.dragTrackId = ""
|
||||||
|
root.dragRow = null
|
||||||
|
root.dragRowNumber = 0
|
||||||
root.dropSlot = -1
|
root.dropSlot = -1
|
||||||
root.dropHot = false
|
root.dropHot = false
|
||||||
|
QbzShell.dragInlineVisual = false
|
||||||
if (shouldCommit)
|
if (shouldCommit)
|
||||||
QbzQueue.queueExtendedDrop(phase, phaseIndex, trackId, slot)
|
QbzQueue.queueExtendedDrop(phase, phaseIndex, trackId, slot)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function dragProxyY() {
|
||||||
|
var p = listHost.mapFromItem(null, QbzShell.dragX, QbzShell.dragY)
|
||||||
|
return Math.max(queueList.y, Math.min(queueList.y + queueList.height - 50,
|
||||||
|
p.y - 25))
|
||||||
|
}
|
||||||
|
|
||||||
Connections {
|
Connections {
|
||||||
target: QbzShell
|
target: QbzShell
|
||||||
function onDragXChanged() { root.recomputeDrop() }
|
function onDragXChanged() { root.recomputeDrop() }
|
||||||
@@ -538,6 +578,9 @@ Rectangle {
|
|||||||
readonly property var displayItem: Object.assign({}, modelData, {
|
readonly property var displayItem: Object.assign({}, modelData, {
|
||||||
"artPath": root.coverPath(modelData)
|
"artPath": root.coverPath(modelData)
|
||||||
})
|
})
|
||||||
|
readonly property bool isDragSource: root.dragTrackId !== ""
|
||||||
|
&& root.dragPhase === modelData.phase
|
||||||
|
&& root.dragPhaseIndex === modelData.phaseIndex
|
||||||
|
|
||||||
width: queueList.width
|
width: queueList.width
|
||||||
height: (heading !== "" ? 24 : 0) + 50
|
height: (heading !== "" ? 24 : 0) + 50
|
||||||
@@ -573,10 +616,17 @@ Rectangle {
|
|||||||
showFavorite: false
|
showFavorite: false
|
||||||
showDownload: false
|
showDownload: false
|
||||||
showMenu: true
|
showMenu: true
|
||||||
|
showReorder: !root.searchActive
|
||||||
|
&& rowHost.modelData.phase === "upcoming"
|
||||||
|
&& !root.rowBlocked(rowHost.modelData)
|
||||||
|
canMoveUp: rowHost.modelData.phaseIndex > 0
|
||||||
|
canMoveDown: rowHost.modelData.phaseIndex
|
||||||
|
< (root.doc.upcomingCount || 0) - 1
|
||||||
zebra: true
|
zebra: true
|
||||||
artistLink: true
|
artistLink: true
|
||||||
draggable: root.dragAllowed(rowHost.modelData)
|
draggable: root.dragAllowed(rowHost.modelData)
|
||||||
reorderDrag: true
|
reorderDrag: true
|
||||||
|
inlineDragVisual: true
|
||||||
playBlocked: root.rowBlocked(rowHost.modelData)
|
playBlocked: root.rowBlocked(rowHost.modelData)
|
||||||
activeBackground: true
|
activeBackground: true
|
||||||
overrideActiveMatch: true
|
overrideActiveMatch: true
|
||||||
@@ -588,9 +638,14 @@ Rectangle {
|
|||||||
&& root.coverPath(rowHost.modelData) === ""
|
&& root.coverPath(rowHost.modelData) === ""
|
||||||
skelPhase: (Math.floor(Math.abs(QbzShell.pulseMs) / 900) % 2) === 1
|
skelPhase: (Math.floor(Math.abs(QbzShell.pulseMs) / 900) % 2) === 1
|
||||||
artSettleMs: 2500
|
artSettleMs: 2500
|
||||||
|
// Keep the MouseArea alive until release while the
|
||||||
|
// complete visual row is carried by `dragProxy`.
|
||||||
|
opacity: rowHost.isDragSource ? 0 : 1
|
||||||
|
|
||||||
onPlayRequested: root.rowPlay(rowHost.modelData)
|
onPlayRequested: root.rowPlay(rowHost.modelData)
|
||||||
onBodyDragStarted: root.beginQueueDrag(rowHost.modelData)
|
onMoveUpRequested: root.moveQueueRow(rowHost.modelData, -1)
|
||||||
|
onMoveDownRequested: root.moveQueueRow(rowHost.modelData, 1)
|
||||||
|
onBodyDragStarted: root.beginQueueDrag(rowHost.modelData, rowHost.index)
|
||||||
onMenuActionRequested: function (action) {
|
onMenuActionRequested: function (action) {
|
||||||
root.menuAction(rowHost.modelData, action)
|
root.menuAction(rowHost.modelData, action)
|
||||||
}
|
}
|
||||||
@@ -606,8 +661,56 @@ Rectangle {
|
|||||||
target: queueList
|
target: queueList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// QueueView's drag visual is the row itself, not the generic
|
||||||
|
// title/subtitle pill. The source delegate stays alive but fully
|
||||||
|
// transparent so it retains the mouse grab; this identical row
|
||||||
|
// follows the pointer over the list. Leaving the list hides it and
|
||||||
|
// re-enables AppShell's compact ghost for sidebar playlist drops.
|
||||||
|
TrackRow {
|
||||||
|
id: dragProxy
|
||||||
|
visible: QbzShell.dragActive && root.dropHot && root.dragRow !== null
|
||||||
|
enabled: false
|
||||||
|
z: 20
|
||||||
|
x: queueList.x
|
||||||
|
y: root.dragProxyY()
|
||||||
|
width: queueList.width
|
||||||
|
item: root.dragDisplayItem
|
||||||
|
number: root.dragRowNumber
|
||||||
|
showArtwork: true
|
||||||
|
showAlbum: root.width >= 720
|
||||||
|
showFavorite: false
|
||||||
|
showDownload: false
|
||||||
|
showMenu: true
|
||||||
|
showReorder: root.dragRow !== null
|
||||||
|
&& root.dragRow.phase === "upcoming"
|
||||||
|
&& !root.searchActive
|
||||||
|
&& !root.rowBlocked(root.dragRow)
|
||||||
|
canMoveUp: root.dragRow !== null && root.dragRow.phaseIndex > 0
|
||||||
|
canMoveDown: root.dragRow !== null && root.dragRow.phaseIndex
|
||||||
|
< (root.doc.upcomingCount || 0) - 1
|
||||||
|
zebra: true
|
||||||
|
artistLink: true
|
||||||
|
draggable: false
|
||||||
|
reorderDrag: false
|
||||||
|
playBlocked: root.dragRow !== null && root.rowBlocked(root.dragRow)
|
||||||
|
activeBackground: true
|
||||||
|
overrideActiveMatch: true
|
||||||
|
activeMatch: root.dragRow !== null && root.dragRow.phase === "current"
|
||||||
|
leadingMarkerIcon: root.dragRow !== null
|
||||||
|
&& root.dragRow.id === (root.doc.stopAfterId || "")
|
||||||
|
? "circle-stop" : ""
|
||||||
|
menuEntriesOverride: root.dragRow !== null
|
||||||
|
? root.queueMenu(root.dragRow) : []
|
||||||
|
artPending: root.dragRow !== null
|
||||||
|
&& (root.dragRow.artUrl || "") !== ""
|
||||||
|
&& root.coverPath(root.dragRow) === ""
|
||||||
|
skelPhase: (Math.floor(Math.abs(QbzShell.pulseMs) / 900) % 2) === 1
|
||||||
|
artSettleMs: 2500
|
||||||
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
visible: root.dropHot && root.dropLineY >= 0
|
visible: root.dropHot && root.dropLineY >= 0
|
||||||
|
z: 30
|
||||||
x: queueList.x + theme.spacingSm
|
x: queueList.x + theme.spacingSm
|
||||||
y: Math.max(0, Math.min(parent.height - height, root.dropLineY))
|
y: Math.max(0, Math.min(parent.height - height, root.dropLineY))
|
||||||
width: queueList.width - 2 * theme.spacingSm
|
width: queueList.width - 2 * theme.spacingSm
|
||||||
|
|||||||
@@ -1901,7 +1901,14 @@ static DRAG_OVER: Mutex<String> = Mutex::new(String::new());
|
|||||||
/// is — `drag_end` runs off the Qt thread and must not read the bridge.
|
/// is — `drag_end` runs off the Qt thread and must not read the bridge.
|
||||||
static DRAG_OVER_QUEUE: Mutex<i32> = Mutex::new(-1);
|
static DRAG_OVER_QUEUE: Mutex<i32> = Mutex::new(-1);
|
||||||
|
|
||||||
pub(crate) fn drag_start(track_id: String, title: String, subtitle: String, x: f32, y: f32) {
|
pub(crate) fn drag_start(
|
||||||
|
track_id: String,
|
||||||
|
title: String,
|
||||||
|
subtitle: String,
|
||||||
|
x: f32,
|
||||||
|
y: f32,
|
||||||
|
inline_visual: bool,
|
||||||
|
) {
|
||||||
let id = track_id.parse::<u64>().unwrap_or(0);
|
let id = track_id.parse::<u64>().unwrap_or(0);
|
||||||
*DRAGGED.lock().unwrap() = if id > 0 { vec![id] } else { Vec::new() };
|
*DRAGGED.lock().unwrap() = if id > 0 { vec![id] } else { Vec::new() };
|
||||||
log::info!("[qbz-qt][drag] start {track_id} ({title})");
|
log::info!("[qbz-qt][drag] start {track_id} ({title})");
|
||||||
@@ -1912,6 +1919,7 @@ pub(crate) fn drag_start(track_id: String, title: String, subtitle: String, x: f
|
|||||||
.set_drag_subtitle(QString::from(subtitle.as_str()));
|
.set_drag_subtitle(QString::from(subtitle.as_str()));
|
||||||
b.as_mut().set_drag_x(x);
|
b.as_mut().set_drag_x(x);
|
||||||
b.as_mut().set_drag_y(y);
|
b.as_mut().set_drag_y(y);
|
||||||
|
b.as_mut().set_drag_inline_visual(inline_visual);
|
||||||
b.as_mut().set_drag_over_playlist_id(QString::default());
|
b.as_mut().set_drag_over_playlist_id(QString::default());
|
||||||
b.as_mut().set_drag_over_queue_index(-1);
|
b.as_mut().set_drag_over_queue_index(-1);
|
||||||
b.as_mut().set_drag_active(true);
|
b.as_mut().set_drag_active(true);
|
||||||
@@ -1945,6 +1953,7 @@ pub(crate) fn drag_end() {
|
|||||||
let queue_slot = std::mem::replace(&mut *DRAG_OVER_QUEUE.lock().unwrap(), -1);
|
let queue_slot = std::mem::replace(&mut *DRAG_OVER_QUEUE.lock().unwrap(), -1);
|
||||||
shell_bridge::ui(|mut b| {
|
shell_bridge::ui(|mut b| {
|
||||||
b.as_mut().set_drag_active(false);
|
b.as_mut().set_drag_active(false);
|
||||||
|
b.as_mut().set_drag_inline_visual(false);
|
||||||
b.as_mut().set_drag_over_playlist_id(QString::default());
|
b.as_mut().set_drag_over_playlist_id(QString::default());
|
||||||
b.as_mut().set_drag_over_queue_index(-1);
|
b.as_mut().set_drag_over_queue_index(-1);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -379,6 +379,11 @@ pub mod qbz_shell {
|
|||||||
#[qproperty(QString, drag_subtitle)]
|
#[qproperty(QString, drag_subtitle)]
|
||||||
#[qproperty(f32, drag_x)]
|
#[qproperty(f32, drag_x)]
|
||||||
#[qproperty(f32, drag_y)]
|
#[qproperty(f32, drag_y)]
|
||||||
|
// True while the drag source supplies a full-size inline visual. The
|
||||||
|
// window-level compact pill stands down in that region; if the row
|
||||||
|
// leaves it (for example toward a sidebar playlist), the source flips
|
||||||
|
// this off and the portable ghost resumes.
|
||||||
|
#[qproperty(bool, drag_inline_visual)]
|
||||||
#[qproperty(QString, drag_over_playlist_id)]
|
#[qproperty(QString, drag_over_playlist_id)]
|
||||||
// The QUEUE drop target: the upcoming-list SLOT the dragged row would
|
// The QUEUE drop target: the upcoming-list SLOT the dragged row would
|
||||||
// be inserted at, or -1 when the pointer is not over the queue. Two
|
// be inserted at, or -1 when the pointer is not over the queue. Two
|
||||||
@@ -684,6 +689,7 @@ pub mod qbz_shell {
|
|||||||
subtitle: QString,
|
subtitle: QString,
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
|
inline_visual: bool,
|
||||||
);
|
);
|
||||||
#[qinvokable]
|
#[qinvokable]
|
||||||
fn drag_move(self: Pin<&mut QbzShell>, x: f32, y: f32);
|
fn drag_move(self: Pin<&mut QbzShell>, x: f32, y: f32);
|
||||||
@@ -815,6 +821,7 @@ pub struct QbzShellRust {
|
|||||||
drag_subtitle: QString,
|
drag_subtitle: QString,
|
||||||
drag_x: f32,
|
drag_x: f32,
|
||||||
drag_y: f32,
|
drag_y: f32,
|
||||||
|
drag_inline_visual: bool,
|
||||||
drag_over_playlist_id: QString,
|
drag_over_playlist_id: QString,
|
||||||
drag_over_queue_index: i32,
|
drag_over_queue_index: i32,
|
||||||
filter_tip_json: QString,
|
filter_tip_json: QString,
|
||||||
@@ -930,6 +937,7 @@ impl Default for QbzShellRust {
|
|||||||
drag_subtitle: QString::default(),
|
drag_subtitle: QString::default(),
|
||||||
drag_x: 0.0,
|
drag_x: 0.0,
|
||||||
drag_y: 0.0,
|
drag_y: 0.0,
|
||||||
|
drag_inline_visual: false,
|
||||||
drag_over_playlist_id: QString::default(),
|
drag_over_playlist_id: QString::default(),
|
||||||
// -1 = the pointer is not over the queue. NOT 0, which is a valid
|
// -1 = the pointer is not over the queue. NOT 0, which is a valid
|
||||||
// slot (drop at the very top of upcoming).
|
// slot (drop at the very top of upcoming).
|
||||||
@@ -1341,6 +1349,7 @@ impl qbz_shell::QbzShell {
|
|||||||
subtitle: QString,
|
subtitle: QString,
|
||||||
x: f32,
|
x: f32,
|
||||||
y: f32,
|
y: f32,
|
||||||
|
inline_visual: bool,
|
||||||
) {
|
) {
|
||||||
crate::drag_start(
|
crate::drag_start(
|
||||||
track_id.to_string(),
|
track_id.to_string(),
|
||||||
@@ -1348,6 +1357,7 @@ impl qbz_shell::QbzShell {
|
|||||||
subtitle.to_string(),
|
subtitle.to_string(),
|
||||||
x,
|
x,
|
||||||
y,
|
y,
|
||||||
|
inline_visual,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user