Files
Button/qml/Main.qml
T
GrigoandClaude Opus 5.5 f07e153f09 Boot splash and periodic announcement banner
Splash: resources/splash/splash.gif built into the binary, played once by
SplashOverlay over a black cover present from the first frame (no UI
flash), then fades into the UI; hides icons loading. GIF, so no
QtMultimedia/gstreamer. Toggle: splash.enabled.

Banner (InfoBanner): metro-style band that fades/slides in with a hold
pictogram; text that does not fit runs as a seamless ticker for two
rounds. Never takes touches; hidden during splash and settings. Config
section banner (enabled, text, intervalSec, showSec, position,
accentColor), editable in the web UI and on-device settings; shown once
after saving.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-10 10:58:16 +00:00

339 lines
11 KiB
QML

import QtQuick 2.12
import QtQuick.Window 2.12
import QtQuick.Controls 2.12
import QtQuick.Layouts 1.12
import QtGraphicalEffects 1.12
import QtQuick.VirtualKeyboard 2.4
Window {
id: root
visible: true
width: 480
height: 480
title: "ButtonTask"
color: "black"
visibility: Window.FullScreen
property bool darkMode: true
function hideSensorsIfDisabled() {
if (!settingsContainer.collectorEnabled && mainTabs.currentIndex > 0)
mainTabs.currentIndex = 0
}
BackgroundLayer {
id: bg
anchors.fill: parent
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
spacing: 6
TabBar {
id: mainTabs
Layout.fillWidth: true
Layout.preferredHeight: 36
background: Rectangle {
color: "#161628"
radius: 4
}
TabButton {
text: "Кнопки"
width: implicitWidth
contentItem: Text {
text: parent.text
color: parent.checked ? "#ffffff" : "#aaaaaa"
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.checked ? "#2a2a40" : "transparent"
radius: 4
}
}
TabButton {
id: sensorsTabBtn
text: "Датчики"
visible: settingsContainer.collectorEnabled
width: visible ? implicitWidth : 0
contentItem: Text {
text: parent.text
color: parent.checked ? "#ffffff" : "#aaaaaa"
font.pixelSize: 14
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
color: parent.checked ? "#2a2a40" : "transparent"
radius: 4
}
}
}
StackLayout {
Layout.fillWidth: true
Layout.fillHeight: true
currentIndex: mainTabs.currentIndex
// --- Buttons ---
Item {
id: content
Loader {
id: viewLoader
anchors.fill: parent
sourceComponent: settingsContainer.layoutMode === "list" ? listComp : gridComp
}
Component {
id: gridComp
Item {
id: gridRoot
anchors.fill: parent
readonly property int gridColumns: Math.max(1, settingsContainer.layoutColumns)
readonly property real cellSide: Math.max(64, width / gridColumns)
readonly property int gridRows: buttonsModel.count === 0
? 0
: Math.ceil(buttonsModel.count / gridColumns)
readonly property real gridContentHeight: gridRows * cellSide
readonly property bool gridFits: gridContentHeight > 0
&& gridContentHeight <= height
GridView {
id: gridView
width: gridRoot.width
height: gridRoot.gridFits ? gridRoot.gridContentHeight : gridRoot.height
y: gridRoot.gridFits ? (gridRoot.height - gridRoot.gridContentHeight) / 2 : 0
cellWidth: gridRoot.cellSide
cellHeight: gridRoot.cellSide
model: buttonsModel
clip: true
interactive: !gridRoot.gridFits
boundsBehavior: Flickable.StopAtBounds
delegate: ButtonDelegate {
width: gridView.cellWidth - settingsContainer.layoutSpacing
height: gridView.cellHeight - settingsContainer.layoutSpacing
showLabel: settingsContainer.showLabels
}
}
}
}
Component {
id: listComp
SwipeView {
id: swipeView
anchors.fill: parent
clip: true
interactive: buttonsModel.count > 1
Repeater {
model: buttonsModel
Item {
width: swipeView.width
height: swipeView.height
ButtonDelegate {
anchors.centerIn: parent
width: Math.min(parent.width, parent.height) * 0.8
height: width
showLabel: settingsContainer.showLabels
}
}
}
}
}
Text {
anchors.centerIn: parent
visible: buttonsModel.count === 0
text: "Нет кнопок.\nОткройте настройки и добавьте кнопку."
color: root.darkMode ? "#cccccc" : "#222222"
font.pixelSize: 18
horizontalAlignment: Text.AlignHCenter
}
}
// --- Sensors ---
SensorsPage {
id: sensorsPage
}
}
}
// Hidden hotspot: double-tap the top-right corner to open settings.
MouseArea {
id: settingsHotspot
anchors.right: parent.right
anchors.top: parent.top
width: 72
height: 72
z: 20
onDoubleClicked: passwordDialog.open()
}
// Display recovery corner: holding it 10 s re-inits the panel (detected in
// C++ RecoveryGesture). This area only shields buttons underneath; a
// button reaching into the corner does not react to touches there.
MouseArea {
id: recoveryHotspot
anchors.left: parent.left
anchors.bottom: parent.bottom
width: 72
height: 72
z: 20
}
// Periodic announcement ("удерживайте кнопку"); config section "banner".
InfoBanner {
id: infoBanner
anchors.left: parent.left
anchors.right: parent.right
anchors.margins: 12
y: settingsContainer.bannerPosition === "top"
? 12 : parent.height - height - 12
z: 30
suppressed: splash.active || root.secureDialogOpen
}
// Boot splash over everything in the window (dialogs live in the overlay
// layer, but none can be open this early).
SplashOverlay {
id: splash
z: 900
}
Connections {
target: settingsContainer
onChanged: root.hideSensorsIfDisabled()
}
Component.onCompleted: hideSensorsIfDisabled()
readonly property bool secureDialogOpen: passwordDialog.opened || settingsDialog.opened
onSecureDialogOpenChanged: uiIdle.active = secureDialogOpen
Connections {
target: uiIdle
onIdle: {
if (passwordDialog.opened)
passwordDialog.close()
if (settingsDialog.opened)
settingsDialog.close()
}
}
Dialog {
id: passwordDialog
modal: true
title: "Пароль"
anchors.centerIn: parent
width: Math.min(420, parent.width * 0.8)
closePolicy: Popup.CloseOnEscape
standardButtons: Dialog.Ok | Dialog.Cancel
contentItem: ColumnLayout {
spacing: 8
Item {
Layout.fillWidth: true
Layout.preferredHeight: 40
TextField {
id: pwdField
anchors.fill: parent
placeholderText: "Введите пароль"
echoMode: TextInput.Password
selectByMouse: false
onAccepted: passwordDialog.accept()
onTextChanged: {
passwordDialog.showLast = length > 0
if (length === 0)
maskTimer.stop()
else
maskTimer.restart()
if (cursorPosition !== length)
cursorPosition = length
}
onCursorPositionChanged: {
if (cursorPosition !== length)
cursorPosition = length
}
}
Rectangle {
visible: passwordDialog.showLast && pwdField.length > 0
color: pwdField.palette.base
readonly property real cell: Math.max(
lastChar.implicitWidth,
(pwdField.cursorRectangle.x - pwdField.leftPadding) / pwdField.length)
x: pwdField.cursorRectangle.x - cell
y: pwdField.cursorRectangle.y
width: cell
height: pwdField.cursorRectangle.height
Text {
id: lastChar
anchors.centerIn: parent
text: pwdField.text.charAt(pwdField.text.length - 1)
font: pwdField.font
color: pwdField.palette.text
}
}
}
Text {
id: wrongPwd
Layout.fillWidth: true
visible: false
color: "red"
text: "Неверный пароль"
}
}
property bool showLast: false
Timer {
id: maskTimer
interval: 1000
onTriggered: passwordDialog.showLast = false
}
onOpened: {
pwdField.text = ""
showLast = false
wrongPwd.visible = false
pwdField.forceActiveFocus()
}
onAccepted: {
if (btnController.checkPassword(pwdField.text)) {
wrongPwd.visible = false
pwdField.text = ""
settingsDialog.open()
} else {
wrongPwd.visible = true
Qt.callLater(passwordDialog.open)
}
}
}
SettingsDialog {
id: settingsDialog
onBannerPreview: infoBanner.preview()
}
InputPanel {
id: inputPanel
parent: Overlay.overlay
z: 999
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
visible: active
}
}