Files
whisper_voice/src/ui/qml/Overlay.qml
T

379 lines
14 KiB
QML

import QtQuick
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Particles
import Qt5Compat.GraphicalEffects
ApplicationWindow {
id: root
width: 460 * (ui ? ui.uiScale : 1.0)
height: 180 * (ui ? ui.uiScale : 1.0)
visible: true
flags: Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint | Qt.Tool
color: "transparent"
title: "WhisperVoice"
Accessible.name: "WhisperVoice Overlay"
FontLoader {
id: jetBrainsMono
source: "fonts/ttf/JetBrainsMono-Bold.ttf"
}
property real shimmerPos: -0.5
property real windowOpacity: ui.getSetting("opacity")
property real uiScale: Number(ui.getSetting("ui_scale")) // Bind Scale
Connections {
target: ui
function onSettingChanged(key, value) {
if (key === "opacity") windowOpacity = value
if (key === "ui_scale") uiScale = Number(value)
}
}
// Visibility Logic
property bool isActive: ui.isRecording || ui.isProcessing
SequentialAnimation {
running: !ui.reduceMotion
loops: Animation.Infinite
PauseAnimation { duration: 3000 }
NumberAnimation {
target: root; property: "shimmerPos"
from: -0.5; to: 1.5; duration: 1500
easing.type: Easing.InOutQuad
}
}
// Container
Item {
id: mainContainer
width: 380
height: 100
anchors.centerIn: parent
// Scale & Opacity Transform
scale: root.isActive ? root.uiScale : 0.8
opacity: root.isActive ? root.windowOpacity : 0.0
visible: opacity > 0.01 // Optimization
// Motion.dev-like Spring Animation
Behavior on scale {
SpringAnimation { spring: 3; damping: 0.25; epsilon: 0.005 }
}
Behavior on opacity {
NumberAnimation { duration: 300; easing.type: Easing.OutCubic }
}
// --- SHADOW ---
Item {
anchors.fill: bgRect
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: "#A0000000"
shadowBlur: 0.4
autoPaddingEnabled: true
}
}
// --- CHASSIS VISUALS (Hidden Source) ---
Item {
id: contentSource
anchors.fill: bgRect
visible: false
// A. Gradient Background
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#CC101015" }
GradientStop { position: 1.0; color: "#CC050505" }
}
}
// B. Animated Gradient Blobs
ShaderEffect {
anchors.fill: parent
opacity: 0.4
visible: !ui.reduceMotion
property real time: 0
fragmentShader: "gradient_blobs.qsb"
NumberAnimation on time { from: 0; to: 1000; duration: 100000; loops: Animation.Infinite }
}
// C. Glow Shader
ShaderEffect {
anchors.fill: parent
opacity: 0.04
visible: !ui.reduceMotion
property real time: 0
property real intensity: ui.amplitude
fragmentShader: "glow.qsb"
NumberAnimation on time { from: 0; to: 100; duration: 10000; loops: Animation.Infinite }
}
// D. Particles
ParticleSystem {
id: particles
anchors.fill: parent
running: !ui.reduceMotion
ItemParticle {
system: particles
delegate: Rectangle { width: 2; height: 2; radius: 1; color: "#10ffffff" }
}
Emitter {
anchors.fill: parent; emitRate: 15; lifeSpan: 4000; size: 4; sizeVariation: 2
velocity: AngleDirection { angle: -90; angleVariation: 180; magnitude: 5 }
acceleration: PointDirection { y: -2 }
}
}
// E. Shimmer
Rectangle {
width: parent.width * 0.4; height: parent.height * 2.5
rotation: 25; anchors.centerIn: parent
x: (root.shimmerPos * parent.width * 2) - width; y: -height/4
gradient: Gradient {
orientation: Gradient.Horizontal
GradientStop { position: 0.0; color: "transparent" }
GradientStop { position: 0.5; color: "#15ffffff" }
GradientStop { position: 1.0; color: "transparent" }
}
opacity: 0.8
}
// F. CRT Shader Effect (Overlay on chassis ONLY)
ShaderEffect {
anchors.fill: parent
visible: !ui.reduceMotion
property real time: 0
fragmentShader: "crt.qsb"
NumberAnimation on time { from: 0; to: 100; duration: 5000; loops: Animation.Infinite }
}
}
// --- MASK ---
Rectangle {
id: contentMask
anchors.fill: bgRect
radius: height / 2
visible: false; color: "white"
smooth: true; antialiasing: true
}
// --- COMPOSITED CHASSIS ---
OpacityMask {
anchors.fill: bgRect
source: contentSource
maskSource: contentMask
}
// --- BORDER & INTERACTION ---
Rectangle {
id: bgRect
anchors.fill: parent
radius: height / 2
color: "transparent"
border.width: 1
border.color: Qt.rgba(1, 1, 1, 0.22)
MouseArea {
anchors.fill: parent; hoverEnabled: true
cursorShape: pressed ? Qt.ClosedHandCursor : Qt.PointingHandCursor
onPressed: root.startSystemMove()
}
Rectangle {
anchors.fill: parent
anchors.margins: ui.isRecording ? -(ui.amplitude * 3) : 0
radius: parent.radius
color: "transparent"
border.width: ui.isRecording ? 3 : 1.5
border.color: ui.isRecording ? "#A0ff4b4b" : "#6000f2ff"
Behavior on anchors.margins {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
Behavior on border.width {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
SequentialAnimation on border.color {
running: ui.isRecording && !ui.reduceMotion
loops: Animation.Infinite
ColorAnimation { from: "#A0ff4b4b"; to: "#C0ff6b6b"; duration: 800 }
ColorAnimation { from: "#C0ff6b6b"; to: "#A0ff4b4b"; duration: 800 }
}
}
}
// --- MICROPHONE ICON (Enhanced) ---
Item {
id: micContainer
width: 80; height: 80
anchors.left: parent.left
anchors.leftMargin: 10
anchors.verticalCenter: parent.verticalCenter
activeFocusOnTab: true
Accessible.name: ui.isRecording ? "Stop recording" : "Start recording"
Accessible.role: Accessible.Button
Keys.onReturnPressed: ui.toggleRecordingRequested()
Keys.onSpacePressed: ui.toggleRecordingRequested()
// Make entire button scale with amplitude
scale: ui.isRecording ? (1.0 + ui.amplitude * 0.12) : 1.0
Behavior on scale {
NumberAnimation { duration: 150; easing.type: Easing.OutCubic }
}
// MouseArea at parent level to avoid layer blocking
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
console.log("Microphone clicked! Emitting signal...")
ui.toggleRecordingRequested()
}
}
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: ui.isRecording ? "#FF00a0ff" : "#FF00f2ff"
shadowBlur: 1.0
shadowOpacity: ui.isRecording ? 1.0 : 0.5
}
Rectangle {
id: micCircle
anchors.fill: parent
radius: width / 2
gradient: Gradient {
GradientStop { position: 0.0; color: "#40ffffff" }
GradientStop { position: 1.0; color: "#10ffffff" }
}
border.width: 2; border.color: "#60ffffff"
SequentialAnimation on scale {
running: ui.isRecording && !ui.reduceMotion
loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 1.08; duration: 600; easing.type: Easing.InOutQuad }
NumberAnimation { from: 1.08; to: 1.0; duration: 600; easing.type: Easing.InOutQuad }
}
Image {
id: micIcon
anchors.centerIn: parent
width: 40
height: 40
source: "microphone.svg"
smooth: true
antialiasing: true
mipmap: true
fillMode: Image.PreserveAspectFit
}
}
// Focus ring
Rectangle {
anchors.fill: micCircle
anchors.margins: -4
radius: width / 2
color: "transparent"
border.width: 2
border.color: "#B794F6" // SettingsStyle.accent equivalent
visible: micContainer.activeFocus
}
}
// --- RAINBOW WAVEFORM (Shader) ---
Item {
id: waveformContainer
anchors.left: micContainer.right
anchors.leftMargin: 10
anchors.right: parent.right
anchors.rightMargin: 80
anchors.verticalCenter: parent.verticalCenter
height: 90
ShaderEffect {
anchors.fill: parent
visible: !ui.reduceMotion
property real time: 0
property real amplitude: ui.amplitude
fragmentShader: "rainbow_wave.qsb"
NumberAnimation on time { from: 0; to: 1000; duration: 100000; loops: Animation.Infinite }
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: Qt.hsla((Date.now() / 100) % 1.0, 1.0, 0.6, 1.0)
shadowBlur: 1.0; shadowOpacity: 1.0
}
}
}
// --- RECORDING TIMER ---
Item {
id: recordingTimerContainer
anchors.right: parent.right
anchors.rightMargin: 20
anchors.verticalCenter: parent.verticalCenter
width: 60; height: 30
property int recordingSeconds: 0
Connections {
target: ui
function onIsRecordingChanged() {
if (!ui.isRecording) recordingTimerContainer.recordingSeconds = 0
}
}
Timer {
interval: 1000; running: ui.isRecording; repeat: true
onTriggered: recordingTimerContainer.recordingSeconds++
}
// Triple-layer glow for REALLY strong effect
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: ui.isRecording ? "#FFff0000" : "#FFffffff"
shadowBlur: 1.0
shadowOpacity: 1.0
shadowHorizontalOffset: 0
shadowVerticalOffset: 0
}
Item {
anchors.fill: parent
layer.enabled: true
layer.effect: MultiEffect {
shadowEnabled: true
shadowColor: ui.isRecording ? "#FFff3030" : "#FFe0e0e5"
shadowBlur: 0.8
shadowOpacity: 1.0
}
Text {
anchors.centerIn: parent
text: {
var mins = Math.floor(recordingTimerContainer.recordingSeconds / 60)
var secs = recordingTimerContainer.recordingSeconds % 60
return (mins < 10 ? "0" : "") + mins + ":" + (secs < 10 ? "0" : "") + secs
}
color: ui.isRecording ? "#ffffff" : "#ffffff"
font.family: jetBrainsMono.name; font.pixelSize: 16; font.bold: true; font.letterSpacing: 2
style: Text.Outline
styleColor: ui.isRecording ? "#ff0000" : "#808085"
Accessible.role: Accessible.StaticText
Accessible.name: "Recording time: " + text
SequentialAnimation on opacity {
running: ui.isRecording && !ui.reduceMotion; loops: Animation.Infinite
NumberAnimation { from: 1.0; to: 0.7; duration: 800 }
NumberAnimation { from: 0.7; to: 1.0; duration: 800 }
}
}
}
}
}
}