379 lines
14 KiB
QML
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 }
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|