import QtQuick import QtQuick.Layouts import qEmby 1.0 Item { id: root property int defaultDuration: 4000 property int maximumVisible: 5 property int nextToastId: 1 function show(message, kind, duration) { const normalizedKind = ["info", "success", "warning", "error"].indexOf(kind) >= 0 ? kind : "info"; const timeout = duration === undefined ? defaultDuration : Math.max(0, duration); const toastId = nextToastId++; toastModel.append({ "toastId": toastId, "message": String(message), "kind": normalizedKind, "timeoutMs": timeout }); while (toastModel.count > maximumVisible)toastModel.remove(0) return toastId; } function info(message, duration) { return show(message, "info", duration); } function success(message, duration) { return show(message, "success", duration); } function warning(message, duration) { return show(message, "warning", duration); } function error(message, duration) { return show(message, "error", duration); } function dismiss(toastId) { for (let i = 0; i < toastModel.count; ++i) { if (toastModel.get(i).toastId === toastId) { toastModel.remove(i); return ; } } } function clear() { toastModel.clear(); } ListModel { id: toastModel } ListView { id: toastView anchors.top: parent.top anchors.right: parent.right anchors.topMargin: Theme.spacingLg anchors.rightMargin: Theme.spacingLg width: Math.min(360, Math.max(240, root.width - Theme.spacingXl * 2)) height: Math.min(contentHeight, Math.max(0, root.height - Theme.spacingXl * 2)) spacing: Theme.spacingSm interactive: contentHeight > height clip: true model: toastModel z: 1000 add: Transition { NumberAnimation { properties: "opacity,x" from: 0 duration: Theme.animationFast easing.type: Easing.OutCubic } } remove: Transition { NumberAnimation { property: "opacity" to: 0 duration: Theme.animationFast } } displaced: Transition { NumberAnimation { properties: "x,y" duration: Theme.animationFast easing.type: Easing.OutCubic } } delegate: Rectangle { id: toast required property int toastId required property string message required property string kind required property int timeoutMs readonly property color statusColor: kind === "success" ? Theme.success : kind === "warning" ? Theme.warning : kind === "error" ? Theme.danger : Theme.accent readonly property string statusIcon: kind === "success" ? "✓" : kind === "warning" ? "!" : kind === "error" ? "×" : "i" readonly property string statusLabel: kind === "success" ? qsTr("Success") : kind === "warning" ? qsTr("Warning") : kind === "error" ? qsTr("Error") : qsTr("Information") width: toastView.width implicitHeight: toastLayout.implicitHeight + Theme.spacingMd * 2 radius: Theme.radiusLg color: Theme.bgSurface border.width: 1 border.color: Theme.alpha(statusColor, 0.72) Accessible.role: Accessible.AlertMessage Accessible.name: qsTr("%1: %2").arg(statusLabel).arg(message) RowLayout { id: toastLayout anchors.fill: parent anchors.margins: Theme.spacingMd spacing: Theme.spacingMd Rectangle { color: toast.statusColor radius: width / 2 Layout.preferredWidth: 24 Layout.preferredHeight: 24 Layout.alignment: Qt.AlignTop Text { anchors.centerIn: parent text: toast.statusIcon color: "#FFFFFF" font.pixelSize: Theme.fontBody font.weight: Font.Bold } } Text { text: toast.message color: Theme.textBody font.pixelSize: Theme.fontBody wrapMode: Text.WordWrap Layout.fillWidth: true Layout.alignment: Qt.AlignVCenter } ThemedButton { text: "×" implicitWidth: 28 implicitHeight: 28 leftPadding: 0 rightPadding: 0 Accessible.name: qsTr("Dismiss notification") Layout.alignment: Qt.AlignTop onClicked: root.dismiss(toast.toastId) } } Timer { interval: toast.timeoutMs running: toast.timeoutMs > 0 repeat: false onTriggered: root.dismiss(toast.toastId) } } } }