add crypto
This commit is contained in:
parent
90cbe489f6
commit
af6a3bce3e
120 changed files with 24616 additions and 462 deletions
|
|
@ -0,0 +1,262 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Authentication area
|
||||
Column {
|
||||
id: authColumn
|
||||
anchors.centerIn: parent
|
||||
anchors.verticalCenterOffset: 80
|
||||
spacing: 20
|
||||
width: 300
|
||||
|
||||
required property bool isVisible
|
||||
required property string errorMessage
|
||||
required property bool isAuthenticating
|
||||
required property bool authSuccess
|
||||
required property string usernameText
|
||||
|
||||
signal passwordEntered(string password)
|
||||
|
||||
// Expose password field
|
||||
readonly property alias passwordField: passwordField
|
||||
|
||||
// Subtle slide up animation (after main slide)
|
||||
transform: Translate {
|
||||
id: authTransform
|
||||
y: isVisible ? 0 : 50
|
||||
Behavior on y {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 600 } // Wait for main slide and time
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 700 } // Wait for time to appear
|
||||
NumberAnimation {
|
||||
duration: 600
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// User avatar with circular masking
|
||||
Rectangle {
|
||||
id: avatarContainer
|
||||
width: 100
|
||||
height: 100
|
||||
radius: 50
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
color: "transparent"
|
||||
border.color: Data.ThemeManager.accentColor
|
||||
border.width: 4
|
||||
clip: true
|
||||
|
||||
// Scale animation for avatar
|
||||
scale: isVisible ? 1.0 : 0.0
|
||||
Behavior on scale {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 1000 } // Wait for auth area to appear
|
||||
NumberAnimation {
|
||||
duration: 400
|
||||
easing.type: Easing.OutBack
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: avatarImage
|
||||
anchors.fill: parent
|
||||
anchors.margins: 4
|
||||
source: Data.Settings.avatarSource
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
cache: false
|
||||
visible: false // Hidden for masking
|
||||
asynchronous: true
|
||||
sourceSize.width: 92
|
||||
sourceSize.height: 92
|
||||
}
|
||||
|
||||
// Apply circular mask to avatar
|
||||
OpacityMask {
|
||||
anchors.fill: avatarImage
|
||||
source: avatarImage
|
||||
maskSource: Rectangle {
|
||||
width: avatarImage.width
|
||||
height: avatarImage.height
|
||||
radius: 46
|
||||
visible: false
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback icon if avatar fails to load
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "person"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 48
|
||||
color: Data.ThemeManager.accentColor
|
||||
visible: avatarImage.status !== Image.Ready
|
||||
}
|
||||
}
|
||||
|
||||
// Username display
|
||||
Text {
|
||||
id: usernameDisplay
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 18
|
||||
color: Data.ThemeManager.primaryText
|
||||
text: usernameText
|
||||
}
|
||||
|
||||
// Password input field
|
||||
Rectangle {
|
||||
width: parent.width
|
||||
height: 50
|
||||
radius: 25
|
||||
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgLighter, 0.4)
|
||||
border.color: passwordField.activeFocus ? Data.ThemeManager.accentColor : Data.ThemeManager.withOpacity(Data.ThemeManager.border, 0.6)
|
||||
border.width: 2
|
||||
|
||||
TextInput {
|
||||
id: passwordField
|
||||
anchors.fill: parent
|
||||
anchors.margins: 15
|
||||
echoMode: TextInput.Normal
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 16
|
||||
color: "transparent" // Hide the actual text
|
||||
selectionColor: Data.ThemeManager.accentColor
|
||||
selectByMouse: true
|
||||
focus: isVisible
|
||||
|
||||
onAccepted: {
|
||||
if (text.length > 0) {
|
||||
passwordEntered(text)
|
||||
}
|
||||
}
|
||||
|
||||
// Password mask with better spaced dots
|
||||
Row {
|
||||
id: passwordDotsRow
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
spacing: 8
|
||||
visible: passwordField.text.length > 0
|
||||
|
||||
property int previousLength: 0
|
||||
|
||||
Repeater {
|
||||
id: passwordRepeater
|
||||
model: passwordField.text.length
|
||||
delegate: Rectangle {
|
||||
id: passwordDot
|
||||
width: 8
|
||||
height: 8
|
||||
radius: 4
|
||||
color: Data.ThemeManager.primaryText
|
||||
|
||||
property bool isNewDot: index >= passwordDotsRow.previousLength
|
||||
|
||||
// Only animate new dots, existing ones stay visible
|
||||
scale: isNewDot ? 0 : 1.0
|
||||
opacity: isNewDot ? 0 : 1.0
|
||||
|
||||
ParallelAnimation {
|
||||
running: passwordDot.isNewDot
|
||||
|
||||
NumberAnimation {
|
||||
target: passwordDot
|
||||
property: "scale"
|
||||
from: 0
|
||||
to: 1.0
|
||||
duration: 200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
|
||||
NumberAnimation {
|
||||
target: passwordDot
|
||||
property: "opacity"
|
||||
from: 0
|
||||
to: 1.0
|
||||
duration: 150
|
||||
easing.type: Easing.OutQuad
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Track length changes to identify new dots
|
||||
Connections {
|
||||
target: passwordField
|
||||
function onTextChanged() {
|
||||
// Update previous length after a short delay to allow new dots to be marked as new
|
||||
Qt.callLater(function() {
|
||||
passwordDotsRow.previousLength = passwordField.text.length
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Placeholder text
|
||||
Text {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
anchors.left: parent.left
|
||||
text: "Password"
|
||||
font.family: passwordField.font.family
|
||||
font.pixelSize: passwordField.font.pixelSize
|
||||
color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.7)
|
||||
visible: passwordField.text.length === 0 && !passwordField.activeFocus
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Error message
|
||||
Text {
|
||||
id: errorText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 14
|
||||
color: Data.ThemeManager.errorColor
|
||||
text: errorMessage
|
||||
visible: errorMessage !== ""
|
||||
wrapMode: Text.WordWrap
|
||||
width: parent.width
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
|
||||
// Authentication status
|
||||
Text {
|
||||
id: statusText
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
font.family: "FiraCode Nerd Font"
|
||||
font.pixelSize: 14
|
||||
color: authSuccess ? Data.ThemeManager.success : Data.ThemeManager.accentColorBright
|
||||
text: {
|
||||
if (authSuccess) return "Authentication successful!"
|
||||
if (isAuthenticating) return "Authenticating..."
|
||||
return ""
|
||||
}
|
||||
visible: isAuthenticating || authSuccess
|
||||
}
|
||||
|
||||
// Public function to clear password
|
||||
function clearPassword() {
|
||||
passwordField.text = ""
|
||||
passwordField.focus = true
|
||||
}
|
||||
|
||||
// Public function to focus password field
|
||||
function focusPassword() {
|
||||
passwordField.focus = true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,299 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Io
|
||||
import "root:/Data" as Data
|
||||
import "root:/Core" as Core
|
||||
|
||||
// Custom lockscreen
|
||||
PanelWindow {
|
||||
id: lockScreen
|
||||
|
||||
required property var shell
|
||||
property bool isLocked: false
|
||||
property bool isAuthenticated: false
|
||||
property string errorMessage: ""
|
||||
property int failedAttempts: 0
|
||||
property bool isAuthenticating: false
|
||||
property bool authSuccess: false
|
||||
property string usernameText: "Enter Password"
|
||||
|
||||
// Animation state - controlled by timer for proper timing
|
||||
property bool animateIn: false
|
||||
|
||||
// Full screen coverage
|
||||
screen: Quickshell.primaryScreen || Quickshell.screens[0]
|
||||
anchors.top: true
|
||||
anchors.left: true
|
||||
anchors.right: true
|
||||
anchors.bottom: true
|
||||
color: "transparent"
|
||||
|
||||
// Top layer to block everything
|
||||
WlrLayershell.layer: WlrLayershell.Overlay
|
||||
WlrLayershell.exclusiveZone: -1
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
WlrLayershell.namespace: "quickshell-lockscreen"
|
||||
|
||||
visible: isLocked
|
||||
|
||||
// Timer for slide-in animation - more reliable than Qt.callLater
|
||||
Timer {
|
||||
id: slideInTimer
|
||||
interval: 100 // Short delay to ensure window is fully rendered
|
||||
running: false
|
||||
onTriggered: {
|
||||
console.log("slideInTimer triggered, setting animateIn = true")
|
||||
animateIn = true
|
||||
}
|
||||
}
|
||||
|
||||
// Timer for slide-out animation before hiding window
|
||||
Timer {
|
||||
id: slideOutTimer
|
||||
interval: 1000 // Wait for slide animation to complete
|
||||
running: false
|
||||
onTriggered: {
|
||||
isLocked = false
|
||||
authArea.clearPassword()
|
||||
errorMessage = ""
|
||||
failedAttempts = 0
|
||||
authSuccess = false
|
||||
}
|
||||
}
|
||||
|
||||
// Timer to show success message before unlocking
|
||||
Timer {
|
||||
id: successTimer
|
||||
interval: 1200 // Show success message for 1.2 seconds
|
||||
running: false
|
||||
onTriggered: {
|
||||
unlock()
|
||||
}
|
||||
}
|
||||
|
||||
// Reset animation state when window becomes invisible
|
||||
onVisibleChanged: {
|
||||
if (!visible) {
|
||||
animateIn = false
|
||||
authSuccess = false
|
||||
slideInTimer.stop()
|
||||
slideOutTimer.stop()
|
||||
successTimer.stop()
|
||||
}
|
||||
}
|
||||
|
||||
// Background component
|
||||
LockscreenBackground {
|
||||
id: background
|
||||
isVisible: lockScreen.visible
|
||||
}
|
||||
|
||||
// Main lockscreen content with slide-from-top animation
|
||||
Item {
|
||||
id: mainContent
|
||||
anchors.fill: parent
|
||||
focus: true // Enable focus for keyboard handling
|
||||
|
||||
// Dramatic slide animation - starts off-screen, slides down when animateIn is true
|
||||
transform: Translate {
|
||||
id: mainTransform
|
||||
y: lockScreen.animateIn ? 0 : -lockScreen.height
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Scale animation for extra drama
|
||||
scale: lockScreen.animateIn ? 1.0 : 0.98
|
||||
Behavior on scale {
|
||||
NumberAnimation {
|
||||
duration: 800
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Keyboard shortcuts
|
||||
Keys.onPressed: function(event) {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
authArea.clearPassword()
|
||||
errorMessage = ""
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
if (authArea.passwordField.text.length > 0) {
|
||||
authenticate(authArea.passwordField.text)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// Authentication area component
|
||||
AuthenticationArea {
|
||||
id: authArea
|
||||
isVisible: lockScreen.animateIn
|
||||
errorMessage: lockScreen.errorMessage
|
||||
isAuthenticating: lockScreen.isAuthenticating
|
||||
authSuccess: lockScreen.authSuccess
|
||||
usernameText: lockScreen.usernameText
|
||||
|
||||
onPasswordEntered: function(password) {
|
||||
authenticate(password)
|
||||
}
|
||||
}
|
||||
|
||||
// Power buttons component
|
||||
PowerButtons {
|
||||
id: powerButtons
|
||||
isVisible: lockScreen.animateIn
|
||||
|
||||
onRebootRequested: rebootProcess.running = true
|
||||
onShutdownRequested: shutdownProcess.running = true
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// Authentication process using proper PAM authentication
|
||||
Process {
|
||||
id: authProcess
|
||||
property string password: ""
|
||||
command: ["sh", "-c", "echo '" + password.replace(/'/g, "'\"'\"'") + "' | sudo -S -k true"]
|
||||
running: false
|
||||
|
||||
onExited: function(exitCode) {
|
||||
isAuthenticating = false
|
||||
|
||||
if (exitCode === 0) {
|
||||
// Authentication successful
|
||||
isAuthenticated = true
|
||||
errorMessage = ""
|
||||
authSuccess = true
|
||||
// Show success message for a brief moment before unlocking
|
||||
successTimer.start()
|
||||
} else {
|
||||
// Authentication failed
|
||||
failedAttempts++
|
||||
errorMessage = failedAttempts === 1 ? "Incorrect password" : `Incorrect password (${failedAttempts} attempts)`
|
||||
authArea.clearPassword()
|
||||
|
||||
// Add delay for failed attempts
|
||||
if (failedAttempts >= 3) {
|
||||
lockoutTimer.start()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Lockout timer for failed attempts
|
||||
Timer {
|
||||
id: lockoutTimer
|
||||
interval: 30000 // 30 second lockout
|
||||
onTriggered: {
|
||||
errorMessage = ""
|
||||
authArea.passwordField.enabled = true
|
||||
authArea.focusPassword()
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot process
|
||||
Process {
|
||||
id: rebootProcess
|
||||
command: ["systemctl", "reboot"]
|
||||
running: false
|
||||
onExited: function(exitCode) { console.log("Reboot process completed with exit code:", exitCode) }
|
||||
}
|
||||
|
||||
// Shutdown process
|
||||
Process {
|
||||
id: shutdownProcess
|
||||
command: ["systemctl", "poweroff"]
|
||||
running: false
|
||||
onExited: function(exitCode) { console.log("Shutdown process completed with exit code:", exitCode) }
|
||||
}
|
||||
|
||||
// Get current username
|
||||
Process {
|
||||
id: usernameProcess
|
||||
command: ["whoami"]
|
||||
running: lockScreen.isLocked
|
||||
|
||||
stdout: SplitParser {
|
||||
onRead: function(data) {
|
||||
const username = data.trim()
|
||||
if (username) {
|
||||
usernameText = username.charAt(0).toUpperCase() + username.slice(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Public functions
|
||||
function lock() {
|
||||
console.log("Lockscreen.lock() called")
|
||||
|
||||
// Reset animation state FIRST, before making window visible
|
||||
animateIn = false
|
||||
|
||||
isLocked = true
|
||||
isAuthenticated = false
|
||||
authSuccess = false
|
||||
errorMessage = ""
|
||||
failedAttempts = 0
|
||||
authArea.clearPassword()
|
||||
usernameProcess.running = true
|
||||
|
||||
// Trigger slide animation after a short delay
|
||||
console.log("Starting slideInTimer")
|
||||
slideInTimer.start()
|
||||
}
|
||||
|
||||
function unlock() {
|
||||
console.log("Lockscreen.unlock() called")
|
||||
|
||||
// Start slide-out animation first
|
||||
animateIn = false
|
||||
|
||||
// Use timer for reliable timing before completely hiding
|
||||
slideOutTimer.start()
|
||||
}
|
||||
|
||||
function authenticate(password) {
|
||||
if (isAuthenticating || password.length === 0) return
|
||||
|
||||
console.log("Authenticating...")
|
||||
isAuthenticating = true
|
||||
authSuccess = false
|
||||
errorMessage = ""
|
||||
|
||||
// Use sudo authentication
|
||||
authProcess.password = password
|
||||
authProcess.running = true
|
||||
}
|
||||
|
||||
// Focus management when locked state changes
|
||||
onIsLockedChanged: {
|
||||
console.log("isLocked changed to:", isLocked)
|
||||
if (isLocked) {
|
||||
mainContent.focus = true
|
||||
authArea.focusPassword()
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up processes on destruction
|
||||
Component.onDestruction: {
|
||||
if (authProcess.running) authProcess.running = false
|
||||
if (rebootProcess.running) rebootProcess.running = false
|
||||
if (shutdownProcess.running) shutdownProcess.running = false
|
||||
if (usernameProcess.running) usernameProcess.running = false
|
||||
if (lockoutTimer.running) lockoutTimer.running = false
|
||||
if (slideInTimer.running) slideInTimer.running = false
|
||||
if (slideOutTimer.running) slideOutTimer.running = false
|
||||
if (successTimer.running) successTimer.running = false
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,59 @@
|
|||
import QtQuick
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Background with wallpaper
|
||||
Rectangle {
|
||||
id: backgroundContainer
|
||||
anchors.fill: parent
|
||||
color: Data.ThemeManager.bgColor
|
||||
|
||||
required property bool isVisible
|
||||
|
||||
// Fade-in animation for the whole background
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
// Wallpaper background
|
||||
Image {
|
||||
id: wallpaperImage
|
||||
anchors.fill: parent
|
||||
source: Data.WallpaperManager.currentWallpaper ? "file://" + Data.WallpaperManager.currentWallpaper : ""
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
asynchronous: true
|
||||
cache: false
|
||||
}
|
||||
|
||||
// Dark overlay
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: Data.ThemeManager.withOpacity(Data.ThemeManager.bgColor, 0.8)
|
||||
}
|
||||
|
||||
// Blur effect overlay
|
||||
GaussianBlur {
|
||||
anchors.fill: wallpaperImage
|
||||
source: wallpaperImage
|
||||
radius: 32
|
||||
samples: 65
|
||||
|
||||
// Blur animation - starts less blurred and increases
|
||||
Behavior on radius {
|
||||
NumberAnimation {
|
||||
duration: 1200
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
if (isVisible) {
|
||||
radius = 32
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,87 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Reboot and shutdown buttons positioned at bottom right
|
||||
Row {
|
||||
anchors.right: parent.right
|
||||
anchors.bottom: parent.bottom
|
||||
anchors.margins: 40
|
||||
spacing: 16
|
||||
|
||||
required property bool isVisible
|
||||
|
||||
signal rebootRequested()
|
||||
signal shutdownRequested()
|
||||
|
||||
// Fade in with delay
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 900 } // Wait for most elements to appear
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reboot button
|
||||
Rectangle {
|
||||
width: 45
|
||||
height: 45
|
||||
radius: 22
|
||||
color: rebootMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
|
||||
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "restart_alt"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 20
|
||||
color: Data.ThemeManager.secondaryText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: rebootMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: rebootRequested()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
|
||||
// Shutdown button
|
||||
Rectangle {
|
||||
width: 45
|
||||
height: 45
|
||||
radius: 22
|
||||
color: shutdownMouseArea.containsMouse ? Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.3) : Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.2)
|
||||
border.color: Data.ThemeManager.withOpacity(Data.ThemeManager.secondaryText, 0.6)
|
||||
border.width: 1
|
||||
|
||||
Text {
|
||||
anchors.centerIn: parent
|
||||
text: "power_settings_new"
|
||||
font.family: "Material Symbols Outlined"
|
||||
font.pixelSize: 18
|
||||
color: Data.ThemeManager.secondaryText
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: shutdownMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
onClicked: shutdownRequested()
|
||||
}
|
||||
|
||||
Behavior on color {
|
||||
ColorAnimation { duration: 200 }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,72 @@
|
|||
import QtQuick
|
||||
import "root:/Data" as Data
|
||||
|
||||
// Time and date display
|
||||
Column {
|
||||
id: timeColumn
|
||||
anchors.top: parent.top
|
||||
anchors.left: parent.left
|
||||
anchors.topMargin: 60
|
||||
anchors.leftMargin: 60
|
||||
spacing: 16
|
||||
|
||||
required property bool isVisible
|
||||
|
||||
// Subtle slide-left animation (after main slide)
|
||||
transform: Translate {
|
||||
id: timeTransform
|
||||
x: isVisible ? 0 : -100
|
||||
Behavior on x {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 400 } // Wait for main slide to be visible
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutQuart
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
opacity: isVisible ? 1.0 : 0.0
|
||||
Behavior on opacity {
|
||||
SequentialAnimation {
|
||||
PauseAnimation { duration: 500 } // Wait for main slide
|
||||
NumberAnimation {
|
||||
duration: 500
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Current time
|
||||
Text {
|
||||
id: timeText
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 84
|
||||
font.weight: Font.ExtraLight
|
||||
color: Data.ThemeManager.brightText
|
||||
text: Qt.formatTime(new Date(), "hh:mm")
|
||||
}
|
||||
|
||||
// Current date
|
||||
Text {
|
||||
id: dateText
|
||||
font.family: "Roboto"
|
||||
font.pixelSize: 28
|
||||
font.weight: Font.Light
|
||||
color: Data.ThemeManager.secondaryText
|
||||
text: Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
|
||||
}
|
||||
|
||||
// Time update timer
|
||||
Timer {
|
||||
id: timeTimer
|
||||
interval: 1000
|
||||
running: isVisible
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
timeText.text = Qt.formatTime(new Date(), "hh:mm")
|
||||
dateText.text = Qt.formatDate(new Date(), "dddd, MMMM d, yyyy")
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue