676 lines
24 KiB
QML
676 lines
24 KiB
QML
pragma ComponentBehavior: Bound
|
|
|
|
import QtQuick
|
|
import QtQuick.Shapes
|
|
import qs.widgets.theme
|
|
|
|
// Renderable visual core for the desktop telemetry rail. Runtime services stay
|
|
// in DenseBar.qml so this Item can be exercised without Wayland or Hyprland.
|
|
Item {
|
|
id: root
|
|
|
|
implicitWidth: 1920
|
|
implicitHeight: 164
|
|
|
|
readonly property bool compact: width <= 1400
|
|
|
|
property bool autoClock: true
|
|
property date now: new Date()
|
|
property string hostName: "LOCAL"
|
|
property bool telemetryReady: false
|
|
property bool ratesReady: false
|
|
property real cpuFraction: 0
|
|
property int cpuThreads: 0
|
|
property real memoryFraction: 0
|
|
property string memoryUsedText: "--"
|
|
property real temperatureCelsius: NaN
|
|
property string networkInterface: "NET"
|
|
property string networkRxText: "--"
|
|
property string networkTxText: "--"
|
|
property real storageFraction: 0
|
|
property string storageFreeText: "-- FREE"
|
|
property var workspaceIds: [1, 2, 3, 4]
|
|
property int activeWorkspaceId: 1
|
|
property int trayCount: 0
|
|
property real audioFraction: 0
|
|
property bool audioMuted: false
|
|
|
|
signal workspaceActivated(int workspaceId)
|
|
|
|
function pct(value, ready) {
|
|
return ready ? Math.round(Math.max(0, Math.min(1, value)) * 100) : 0;
|
|
}
|
|
|
|
function two(value) {
|
|
return value < 10 ? "0" + value : String(value);
|
|
}
|
|
|
|
function timeText(value) {
|
|
return root.two(value.getHours()) + ":" + root.two(value.getMinutes());
|
|
}
|
|
|
|
function dateText(value) {
|
|
const days = ["SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"];
|
|
const months = ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL", "AUG", "SEP", "OCT", "NOV", "DEC"];
|
|
return days[value.getDay()] + " // " + root.two(value.getDate()) + " " + months[value.getMonth()];
|
|
}
|
|
|
|
Timer {
|
|
interval: 30000
|
|
running: root.autoClock
|
|
repeat: true
|
|
triggeredOnStart: true
|
|
onTriggered: root.now = new Date()
|
|
}
|
|
|
|
// Faint drafting grid; no gradient and deliberately subordinate to data.
|
|
Repeater {
|
|
model: Math.ceil(root.width / 40)
|
|
Rectangle {
|
|
required property int index
|
|
x: index * 40
|
|
width: 1
|
|
height: root.height
|
|
color: Theme.text
|
|
opacity: 0.018
|
|
}
|
|
}
|
|
|
|
Repeater {
|
|
model: Math.ceil(root.height / 40)
|
|
Rectangle {
|
|
required property int index
|
|
y: index * 40
|
|
width: root.width
|
|
height: 1
|
|
color: Theme.text
|
|
opacity: 0.018
|
|
}
|
|
}
|
|
|
|
Item {
|
|
id: array
|
|
x: 12
|
|
y: 8
|
|
width: root.width - 24
|
|
height: 92
|
|
|
|
readonly property real identityWidth: root.compact ? 250 : 320
|
|
readonly property real stateWidth: root.compact ? 300 : 350
|
|
readonly property real flexWidth: Math.max(250, (width - identityWidth - stateWidth - 32) / 2)
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
spacing: 8
|
|
|
|
StatusBarPanel {
|
|
width: array.identityWidth
|
|
height: array.height
|
|
panelId: "001"
|
|
title: "COMMAND LAYER"
|
|
|
|
Text {
|
|
x: 12
|
|
y: 34
|
|
text: root.hostName.toUpperCase()
|
|
color: Theme.text
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: root.compact ? 20 : 25
|
|
font.bold: true
|
|
font.letterSpacing: 2
|
|
elide: Text.ElideRight
|
|
width: parent.width - 92
|
|
}
|
|
|
|
Text {
|
|
x: 13
|
|
y: 63
|
|
text: "STATUS ARRAY // " + (root.telemetryReady ? "LIVE" : "STANDBY")
|
|
color: Theme.accent
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 7
|
|
font.letterSpacing: 1.4
|
|
}
|
|
|
|
Rectangle {
|
|
x: parent.width - 75
|
|
y: 36
|
|
width: 1
|
|
height: 43
|
|
color: Theme.hair
|
|
}
|
|
|
|
Rectangle {
|
|
x: parent.width - 62
|
|
y: 42
|
|
width: 5
|
|
height: 5
|
|
color: Theme.accent
|
|
opacity: root.telemetryReady ? 1 : 0.35
|
|
}
|
|
|
|
Text {
|
|
x: parent.width - 50
|
|
y: 38
|
|
text: root.telemetryReady ? "ONLINE" : "LOCAL"
|
|
color: Theme.accent
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 7
|
|
}
|
|
|
|
Text {
|
|
x: parent.width - 63
|
|
y: 57
|
|
text: "NODE // " + (root.hostName || "--").toUpperCase().slice(0, 7)
|
|
color: Theme.muted
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 6
|
|
}
|
|
|
|
Shape {
|
|
x: parent.width - 63
|
|
y: 71
|
|
width: 48
|
|
height: 10
|
|
ShapePath {
|
|
fillColor: "transparent"
|
|
strokeColor: Theme.accent
|
|
strokeWidth: 1
|
|
startX: 0; startY: 6
|
|
PathLine { x: 11; y: 6 }
|
|
PathLine { x: 16; y: 1 }
|
|
PathLine { x: 22; y: 9 }
|
|
PathLine { x: 27; y: 6 }
|
|
PathLine { x: 48; y: 6 }
|
|
}
|
|
}
|
|
}
|
|
|
|
StatusBarPanel {
|
|
width: array.flexWidth
|
|
height: array.height
|
|
panelId: "02"
|
|
title: "RESOURCE LATTICE"
|
|
meta: root.telemetryReady ? "REALTIME" : "FALLBACK"
|
|
|
|
Row {
|
|
x: 11
|
|
y: 32
|
|
width: parent.width - 22
|
|
height: 51
|
|
spacing: root.compact ? 7 : 12
|
|
|
|
MetricBlock {
|
|
width: (parent.width - parent.spacing * (root.compact ? 1 : 2)) / (root.compact ? 2 : 3)
|
|
label: "CPU"
|
|
value: root.cpuFraction
|
|
ready: root.ratesReady
|
|
readout: root.ratesReady ? root.pct(root.cpuFraction, true) + "%" : "--"
|
|
detailLeft: "CORE:" + (root.cpuThreads || "--")
|
|
detailRight: root.ratesReady ? "BUSY" : "PRIME"
|
|
}
|
|
|
|
MetricBlock {
|
|
width: (parent.width - parent.spacing * (root.compact ? 1 : 2)) / (root.compact ? 2 : 3)
|
|
label: "MEM"
|
|
value: root.memoryFraction
|
|
ready: root.telemetryReady
|
|
readout: root.telemetryReady ? root.pct(root.memoryFraction, true) + "%" : "--"
|
|
detailLeft: "ALLOC"
|
|
detailRight: root.memoryUsedText
|
|
}
|
|
|
|
MetricBlock {
|
|
visible: !root.compact
|
|
width: (parent.width - parent.spacing * 2) / 3
|
|
label: "THERM"
|
|
value: isFinite(root.temperatureCelsius) ? root.temperatureCelsius / 100 : 0
|
|
ready: isFinite(root.temperatureCelsius)
|
|
readout: isFinite(root.temperatureCelsius) ? Math.round(root.temperatureCelsius) + "°C" : "--"
|
|
detailLeft: "ZONE:01"
|
|
detailRight: isFinite(root.temperatureCelsius) && root.temperatureCelsius >= 85 ? "HOT" : "NOMINAL"
|
|
}
|
|
}
|
|
}
|
|
|
|
StatusBarPanel {
|
|
width: array.flexWidth
|
|
height: array.height
|
|
panelId: "03"
|
|
title: "CARRIER UPLINK"
|
|
meta: root.networkInterface.toUpperCase()
|
|
|
|
NetworkTrace {
|
|
x: 12
|
|
y: 43
|
|
width: parent.width - (root.compact ? 117 : 145)
|
|
height: 33
|
|
level: root.ratesReady ? root.cpuFraction : 0.25
|
|
}
|
|
|
|
Column {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 14
|
|
y: 34
|
|
width: root.compact ? 92 : 116
|
|
spacing: 1
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.networkRxText
|
|
horizontalAlignment: Text.AlignRight
|
|
color: Theme.accent
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: root.compact ? 11 : 13
|
|
font.bold: true
|
|
elide: Text.ElideLeft
|
|
}
|
|
MicroText { width: parent.width; text: "RX // DOWN"; horizontalAlignment: Text.AlignRight }
|
|
Text {
|
|
width: parent.width
|
|
text: root.networkTxText
|
|
horizontalAlignment: Text.AlignRight
|
|
color: Theme.text
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: root.compact ? 11 : 13
|
|
font.bold: true
|
|
elide: Text.ElideLeft
|
|
}
|
|
MicroText { width: parent.width; text: "TX // UP"; horizontalAlignment: Text.AlignRight }
|
|
}
|
|
}
|
|
|
|
StatusBarPanel {
|
|
width: array.stateWidth
|
|
height: array.height
|
|
panelId: "04"
|
|
title: "SYSTEM STATE"
|
|
|
|
Row {
|
|
x: 10
|
|
y: 35
|
|
spacing: 6
|
|
|
|
StateCell { code: "N"; active: root.ratesReady; label: "NET" }
|
|
StateCell { code: root.audioMuted ? "M" : "A"; active: !root.audioMuted; label: "AUD" }
|
|
StateCell { visible: !root.compact; code: String(root.trayCount); active: root.trayCount > 0; label: "TRAY" }
|
|
}
|
|
|
|
Rectangle {
|
|
x: root.compact ? 101 : 151
|
|
y: 33
|
|
width: 1
|
|
height: 46
|
|
color: Theme.hair
|
|
}
|
|
|
|
Column {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 12
|
|
y: 34
|
|
width: root.compact ? 91 : 82
|
|
spacing: 2
|
|
|
|
Text {
|
|
width: parent.width
|
|
text: root.timeText(root.now)
|
|
horizontalAlignment: Text.AlignRight
|
|
color: Theme.text
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: root.compact ? 20 : 22
|
|
font.bold: true
|
|
font.letterSpacing: 1
|
|
}
|
|
Text {
|
|
width: parent.width
|
|
text: root.dateText(root.now)
|
|
horizontalAlignment: Text.AlignRight
|
|
color: Theme.accent
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 6
|
|
elide: Text.ElideLeft
|
|
}
|
|
MicroText { width: parent.width; text: "LOCAL TIME"; horizontalAlignment: Text.AlignRight }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
StatusBarPanel {
|
|
id: rail
|
|
x: 12
|
|
y: 108
|
|
width: root.width - 24
|
|
height: 34
|
|
showHeader: false
|
|
chamfer: 10
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.leftMargin: 10
|
|
anchors.rightMargin: 10
|
|
|
|
RailSection {
|
|
width: root.compact ? 252 : 310
|
|
Text {
|
|
anchors.left: parent.left
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "06 // DESKTOP"
|
|
color: Theme.accent
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 7
|
|
font.letterSpacing: 1
|
|
}
|
|
Row {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
spacing: 3
|
|
Repeater {
|
|
model: root.workspaceIds.slice(0, root.compact ? 4 : 6)
|
|
Rectangle {
|
|
required property var modelData
|
|
width: 24
|
|
height: 17
|
|
color: Number(modelData) === root.activeWorkspaceId ? Theme.accentAlpha(0.18) : "transparent"
|
|
border.width: 1
|
|
border.color: Number(modelData) === root.activeWorkspaceId ? Theme.accent : Theme.hair
|
|
Text {
|
|
anchors.centerIn: parent
|
|
text: root.two(Number(parent.modelData))
|
|
color: Number(parent.modelData) === root.activeWorkspaceId ? Theme.accent : Theme.muted
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 7
|
|
}
|
|
MouseArea {
|
|
anchors.fill: parent
|
|
onClicked: root.workspaceActivated(Number(parent.modelData))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
RailSection {
|
|
width: root.compact ? 260 : 410
|
|
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "PROCESS" }
|
|
Text {
|
|
x: 76
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: "COMPOSITOR // NODE_EXPORTER // SHELL"
|
|
color: root.telemetryReady ? Theme.text : Theme.muted
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 7
|
|
elide: Text.ElideRight
|
|
width: parent.width - 84
|
|
}
|
|
}
|
|
|
|
RailSection {
|
|
width: root.compact ? 300 : 385
|
|
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "VOL // ROOT" }
|
|
Text {
|
|
x: 93
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.storageFreeText
|
|
color: Theme.text
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 8
|
|
}
|
|
SegmentMeter {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: root.compact ? 95 : 170
|
|
height: 7
|
|
segments: root.compact ? 10 : 16
|
|
value: root.storageFraction
|
|
ready: root.telemetryReady
|
|
}
|
|
}
|
|
|
|
RailSection {
|
|
visible: !root.compact
|
|
width: 350
|
|
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: "AUDIO BUS" }
|
|
Text {
|
|
x: 89
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.audioMuted ? "MUTED" : Math.round(root.audioFraction * 100) + "%"
|
|
color: root.audioMuted ? Theme.muted : Theme.accent
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 8
|
|
}
|
|
SegmentMeter {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 10
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
width: 155
|
|
height: 7
|
|
segments: 16
|
|
value: root.audioFraction
|
|
ready: true
|
|
}
|
|
}
|
|
|
|
RailSection {
|
|
width: Math.max(150, rail.width - (root.compact ? 812 : 1455) - 20)
|
|
borderVisible: false
|
|
MicroText { x: 9; anchors.verticalCenter: parent.verticalCenter; text: root.compact ? "SYS // " + root.timeText(root.now) : "EVENTS" }
|
|
Text {
|
|
anchors.right: parent.right
|
|
anchors.rightMargin: 8
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.telemetryReady ? "WARN:00 // ERR:00" : "TELEMETRY WAIT"
|
|
color: root.telemetryReady ? Theme.muted : Theme.accent
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 7
|
|
elide: Text.ElideLeft
|
|
width: parent.width - 64
|
|
horizontalAlignment: Text.AlignRight
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Bottom routing trace and caution hatch.
|
|
Shape {
|
|
x: 12
|
|
y: 150
|
|
width: root.width - 24
|
|
height: 12
|
|
ShapePath {
|
|
fillColor: "transparent"
|
|
strokeColor: Theme.accentAlpha(0.55)
|
|
strokeWidth: 1
|
|
startX: 0; startY: 1
|
|
PathLine { x: 220; y: 1 }
|
|
PathLine { x: 232; y: 11 }
|
|
PathLine { x: 330; y: 11 }
|
|
}
|
|
}
|
|
|
|
Row {
|
|
x: 360
|
|
y: 151
|
|
width: root.width - 372
|
|
height: 3
|
|
spacing: 5
|
|
clip: true
|
|
Repeater {
|
|
model: Math.ceil(parent.width / 10)
|
|
Rectangle {
|
|
required property int index
|
|
width: 5
|
|
height: 3
|
|
color: index % 2 === 0 ? Theme.accent : "transparent"
|
|
opacity: 0.58
|
|
}
|
|
}
|
|
}
|
|
|
|
component MicroText: Text {
|
|
color: Theme.muted
|
|
font.family: Theme.microFont
|
|
font.pixelSize: 6
|
|
font.letterSpacing: 0.7
|
|
}
|
|
|
|
component RailSection: Item {
|
|
property bool borderVisible: true
|
|
height: rail.height
|
|
Rectangle {
|
|
visible: parent.borderVisible
|
|
anchors.right: parent.right
|
|
width: 1
|
|
height: parent.height
|
|
color: Theme.hair
|
|
opacity: 0.65
|
|
}
|
|
}
|
|
|
|
component MetricBlock: Item {
|
|
id: metric
|
|
property string label: ""
|
|
property real value: 0
|
|
property bool ready: false
|
|
property string readout: "--"
|
|
property string detailLeft: ""
|
|
property string detailRight: ""
|
|
height: 51
|
|
|
|
MicroText { x: 0; y: 1; text: metric.label; font.pixelSize: 7 }
|
|
Text {
|
|
anchors.right: parent.right
|
|
y: -3
|
|
text: metric.readout
|
|
color: Theme.text
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: root.compact ? 14 : 17
|
|
font.bold: true
|
|
}
|
|
SegmentMeter {
|
|
x: 0; y: 19
|
|
width: parent.width
|
|
height: 9
|
|
segments: 10
|
|
value: metric.value
|
|
ready: metric.ready
|
|
}
|
|
MicroText { x: 0; y: 34; text: metric.detailLeft }
|
|
MicroText { anchors.right: parent.right; y: 34; text: metric.detailRight; horizontalAlignment: Text.AlignRight }
|
|
}
|
|
|
|
component SegmentMeter: Row {
|
|
id: meter
|
|
property int segments: 10
|
|
property real value: 0
|
|
property bool ready: false
|
|
spacing: 2
|
|
Repeater {
|
|
model: meter.segments
|
|
Rectangle {
|
|
required property int index
|
|
width: Math.max(2, (meter.width - (meter.segments - 1) * meter.spacing) / meter.segments)
|
|
height: meter.height
|
|
readonly property bool on: meter.ready && index < Math.round(Math.max(0, Math.min(1, meter.value)) * meter.segments)
|
|
color: on ? Theme.accent : Theme.textAlpha(0.06)
|
|
border.width: 1
|
|
border.color: on ? Theme.accent : Theme.textAlpha(0.18)
|
|
}
|
|
}
|
|
}
|
|
|
|
component StateCell: Item {
|
|
id: state
|
|
property string code: "--"
|
|
property string label: ""
|
|
property bool active: false
|
|
width: root.compact ? 39 : 43
|
|
height: 42
|
|
|
|
Shape {
|
|
anchors.fill: parent
|
|
ShapePath {
|
|
fillColor: state.active ? Theme.accentAlpha(0.18) : "transparent"
|
|
strokeColor: state.active ? Theme.accent : Theme.hair
|
|
strokeWidth: 1
|
|
startX: 1; startY: 1
|
|
PathLine { x: parent.width - 7; y: 1 }
|
|
PathLine { x: parent.width - 1; y: 7 }
|
|
PathLine { x: parent.width - 1; y: parent.height - 1 }
|
|
PathLine { x: 7; y: parent.height - 1 }
|
|
PathLine { x: 1; y: parent.height - 7 }
|
|
PathLine { x: 1; y: 1 }
|
|
}
|
|
}
|
|
Text {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
y: 7
|
|
text: state.code
|
|
color: state.active ? Theme.accent : Theme.text
|
|
font.family: Theme.displayFont
|
|
font.pixelSize: 13
|
|
font.bold: true
|
|
}
|
|
MicroText { anchors.horizontalCenter: parent.horizontalCenter; y: 27; text: state.label }
|
|
}
|
|
|
|
component RadarGauge: Item {
|
|
id: radar
|
|
property real size: 58
|
|
property real level: 0
|
|
width: size
|
|
height: size
|
|
|
|
Repeater {
|
|
model: [1, 0.72, 0.36]
|
|
Rectangle {
|
|
required property int index
|
|
required property var modelData
|
|
anchors.centerIn: parent
|
|
width: radar.size * Number(modelData)
|
|
height: width
|
|
radius: width / 2
|
|
color: "transparent"
|
|
border.width: 1
|
|
border.color: index === 0 ? Theme.accent : Theme.hair
|
|
}
|
|
}
|
|
Rectangle { x: 0; y: parent.height / 2; width: parent.width; height: 1; color: Theme.hair }
|
|
Rectangle { x: parent.width / 2; y: 0; width: 1; height: parent.height; color: Theme.hair }
|
|
Rectangle { x: 12; y: 18; width: 4; height: 4; color: Theme.accent }
|
|
Rectangle { x: parent.width - 14; y: parent.height - 20; width: 4; height: 4; color: Theme.accent }
|
|
Rectangle { x: parent.width / 2; y: parent.height - 11; width: 4; height: 4; color: Theme.accent }
|
|
MicroText { anchors.right: parent.right; y: 3; text: "R:" + Math.round(radar.level * 99); color: Theme.accent }
|
|
}
|
|
|
|
component NetworkTrace: Item {
|
|
id: trace
|
|
property real level: 0
|
|
Rectangle { x: 0; y: parent.height - 1; width: parent.width; height: 1; color: Theme.hair }
|
|
Rectangle { x: 0; y: 0; width: 1; height: parent.height; color: Theme.hair }
|
|
Shape {
|
|
anchors.fill: parent
|
|
ShapePath {
|
|
fillColor: "transparent"
|
|
strokeColor: Theme.accent
|
|
strokeWidth: 1.2
|
|
startX: 0; startY: trace.height * 0.65
|
|
PathLine { x: trace.width * 0.10; y: trace.height * 0.65 }
|
|
PathLine { x: trace.width * 0.15; y: trace.height * (0.25 + trace.level * 0.15) }
|
|
PathLine { x: trace.width * 0.21; y: trace.height * 0.78 }
|
|
PathLine { x: trace.width * 0.31; y: trace.height * 0.56 }
|
|
PathLine { x: trace.width * 0.43; y: trace.height * 0.62 }
|
|
PathLine { x: trace.width * 0.49; y: trace.height * 0.18 }
|
|
PathLine { x: trace.width * 0.57; y: trace.height * 0.82 }
|
|
PathLine { x: trace.width * 0.68; y: trace.height * 0.58 }
|
|
PathLine { x: trace.width * 0.79; y: trace.height * 0.62 }
|
|
PathLine { x: trace.width * 0.85; y: trace.height * 0.34 }
|
|
PathLine { x: trace.width * 0.91; y: trace.height * 0.75 }
|
|
PathLine { x: trace.width; y: trace.height * 0.60 }
|
|
}
|
|
}
|
|
}
|
|
}
|