339 lines
11 KiB
QML
339 lines
11 KiB
QML
import QtQuick
|
|
import QtQuick.Layouts
|
|
import QtQuick.Controls
|
|
import Quickshell
|
|
import Quickshell.Wayland
|
|
import Quickshell.Hyprland
|
|
import "../theme"
|
|
import ".."
|
|
|
|
PanelWindow {
|
|
id: dockWindow
|
|
property var modelData
|
|
screen: modelData
|
|
|
|
anchors {
|
|
bottom: true
|
|
}
|
|
|
|
margins {
|
|
bottom: 6
|
|
}
|
|
|
|
// Adjust width to fit content
|
|
implicitWidth: dockTray.implicitWidth + 32
|
|
implicitHeight: 90
|
|
|
|
color: "transparent"
|
|
WlrLayershell.layer: WlrLayer.Top
|
|
WlrLayershell.namespace: "quickshell-dock"
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
|
|
// Reserve 70px exclusive zone so windows don't overlap the dock
|
|
exclusiveZone: 70
|
|
|
|
// Tooltip overlay container
|
|
Item {
|
|
id: tooltipContainer
|
|
anchors.fill: parent
|
|
z: 100
|
|
}
|
|
|
|
// 2.5D Frosted Glass Dock Tray
|
|
Rectangle {
|
|
id: dockTray
|
|
anchors.bottom: parent.bottom
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
implicitWidth: dockLayout.implicitWidth + 24
|
|
height: 68
|
|
radius: Theme.radiusDock
|
|
color: Theme.dockBg
|
|
border.color: Theme.dockBorder
|
|
border.width: 1
|
|
|
|
// Top glossy reflection edge line (Catalina shelf characteristic)
|
|
Rectangle {
|
|
anchors.left: parent.left
|
|
anchors.right: parent.right
|
|
anchors.top: parent.top
|
|
anchors.leftMargin: 12
|
|
anchors.rightMargin: 12
|
|
height: 1
|
|
color: Theme.dockHighlight
|
|
opacity: 0.8
|
|
}
|
|
|
|
// Inner items layout
|
|
RowLayout {
|
|
id: dockLayout
|
|
anchors.centerIn: parent
|
|
spacing: 6
|
|
|
|
// =================================================================
|
|
// 1. PINNED APPS
|
|
// =================================================================
|
|
DockItem {
|
|
id: finderItem
|
|
name: "Finder"
|
|
iconSource: "../assets/finder.svg"
|
|
execCmd: "foot -e sh -c 'cd ~; lf || yazi || ls -la; exec zsh'"
|
|
appClass: "foot"
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "Launchpad"
|
|
iconSource: "../assets/launchpad.svg"
|
|
execCmd: ""
|
|
isPinned: true
|
|
onCustomClicked: GlobalState.toggleSpotlight()
|
|
}
|
|
|
|
DockItem {
|
|
name: "Brave Browser"
|
|
iconSource: "../assets/browser.svg"
|
|
execCmd: "brave"
|
|
appClass: "brave-browser"
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "Terminal"
|
|
iconSource: "../assets/terminal.svg"
|
|
execCmd: "foot"
|
|
appClass: "foot"
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "Neovim"
|
|
iconSource: "../assets/code.svg"
|
|
execCmd: "foot -e nvim"
|
|
appClass: "nvim"
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "System Preferences"
|
|
iconSource: "../assets/preferences.svg"
|
|
execCmd: "pavucontrol"
|
|
appClass: "pavucontrol"
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "App Store"
|
|
iconSource: "../assets/appstore.svg"
|
|
execCmd: "foot -e zsh -c 'echo NixOS Packages; nix-env -qaP | head -n 50; exec zsh'"
|
|
appClass: ""
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "Music"
|
|
iconSource: "../assets/music.svg"
|
|
execCmd: ""
|
|
appClass: ""
|
|
isPinned: true
|
|
onCustomClicked: GlobalState.mediaPlayPause()
|
|
}
|
|
|
|
// =================================================================
|
|
// 2. DYNAMIC RUNNING APPS (Not pinned)
|
|
// =================================================================
|
|
Repeater {
|
|
model: {
|
|
const pinnedClasses = ["foot", "brave-browser", "pavucontrol", "nvim"];
|
|
const list = [];
|
|
const seen = {};
|
|
if (Array.isArray(GlobalState.runningApps)) {
|
|
for (let i = 0; i < GlobalState.runningApps.length; i++) {
|
|
const client = GlobalState.runningApps[i];
|
|
const cls = (client.class || "").toLowerCase();
|
|
if (cls && !pinnedClasses.includes(cls) && !seen[cls]) {
|
|
seen[cls] = true;
|
|
list.push({
|
|
name: client.title || client.class || "App",
|
|
cls: client.class,
|
|
address: client.address
|
|
});
|
|
}
|
|
}
|
|
}
|
|
return list;
|
|
}
|
|
delegate: DockItem {
|
|
required property var modelData
|
|
name: modelData.name
|
|
iconSource: "../assets/code.svg"
|
|
appClass: modelData.cls
|
|
isPinned: false
|
|
onCustomClicked: {
|
|
if (modelData.address) {
|
|
Hyprland.dispatch(`focuswindow address:${modelData.address}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// =================================================================
|
|
// 3. DOCK SEPARATOR DIVIDER
|
|
// =================================================================
|
|
Rectangle {
|
|
Layout.preferredWidth: 1
|
|
Layout.preferredHeight: 44
|
|
Layout.alignment: Qt.AlignVCenter
|
|
Layout.leftMargin: 4
|
|
Layout.rightMargin: 4
|
|
color: Qt.rgba(1.0, 1.0, 1.0, 0.22)
|
|
}
|
|
|
|
// =================================================================
|
|
// 4. DOWNLOADS FOLDER & TRASH
|
|
// =================================================================
|
|
DockItem {
|
|
name: "Downloads"
|
|
iconSource: "../assets/folder-downloads.svg"
|
|
execCmd: "foot -e sh -c 'cd ~/Downloads; ls -la; exec zsh'"
|
|
isPinned: true
|
|
}
|
|
|
|
DockItem {
|
|
name: "Trash"
|
|
iconSource: "../assets/trash-empty.svg"
|
|
execCmd: "foot -e sh -c 'cd ~/.local/share/Trash/files 2>/dev/null || cd ~; ls -la; exec zsh'"
|
|
isPinned: true
|
|
}
|
|
}
|
|
}
|
|
|
|
// Reusable Dock Item Component
|
|
component DockItem: Item {
|
|
id: itemRoot
|
|
property string name: ""
|
|
property string iconSource: ""
|
|
property string execCmd: ""
|
|
property string appClass: ""
|
|
property bool isPinned: true
|
|
signal customClicked()
|
|
|
|
// Check if app is running
|
|
readonly property bool isRunning: {
|
|
if (!itemRoot.appClass) return false;
|
|
const target = itemRoot.appClass.toLowerCase();
|
|
if (GlobalState.activeWindowClass.toLowerCase().includes(target)) return true;
|
|
if (Array.isArray(GlobalState.runningApps)) {
|
|
for (let i = 0; i < GlobalState.runningApps.length; i++) {
|
|
const c = GlobalState.runningApps[i];
|
|
if ((c.class || "").toLowerCase().includes(target)) return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
implicitWidth: 50
|
|
implicitHeight: 64
|
|
Layout.alignment: Qt.AlignBottom
|
|
|
|
// Container with scale & bounce transform
|
|
Item {
|
|
id: iconWrapper
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 8
|
|
width: 48
|
|
height: 48
|
|
|
|
scale: itemArea.containsMouse ? 1.28 : 1.0
|
|
y: itemArea.containsMouse ? -8 : 0
|
|
|
|
Behavior on scale {
|
|
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
|
}
|
|
Behavior on y {
|
|
NumberAnimation { duration: 180; easing.type: Easing.OutCubic }
|
|
}
|
|
|
|
// Icon Image
|
|
Image {
|
|
anchors.centerIn: parent
|
|
width: 46
|
|
height: 46
|
|
source: itemRoot.iconSource
|
|
sourceSize: Qt.size(92, 92)
|
|
smooth: true
|
|
mipmap: true
|
|
}
|
|
|
|
// Click Bounce Sequential Animation
|
|
SequentialAnimation {
|
|
id: bounceAnim
|
|
NumberAnimation { target: iconWrapper; property: "y"; to: -24; duration: 150; easing.type: Easing.OutQuad }
|
|
NumberAnimation { target: iconWrapper; property: "y"; to: -4; duration: 130; easing.type: Easing.InQuad }
|
|
NumberAnimation { target: iconWrapper; property: "y"; to: -14; duration: 100; easing.type: Easing.OutQuad }
|
|
NumberAnimation { target: iconWrapper; property: "y"; to: 0; duration: 100; easing.type: Easing.InQuad }
|
|
}
|
|
}
|
|
|
|
// Active App Running Indicator Dot (White glowing pill)
|
|
Rectangle {
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: 2
|
|
width: 4
|
|
height: 4
|
|
radius: 2
|
|
color: Theme.dockDot
|
|
visible: itemRoot.isRunning
|
|
}
|
|
|
|
// Hover Tooltip Badge (macOS Catalina Style Black Pill)
|
|
Rectangle {
|
|
id: tooltipPill
|
|
visible: itemArea.containsMouse
|
|
opacity: itemArea.containsMouse ? 1.0 : 0.0
|
|
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.top
|
|
anchors.bottomMargin: 6
|
|
implicitWidth: tooltipText.implicitWidth + 16
|
|
height: 24
|
|
radius: 6
|
|
color: Qt.rgba(0.08, 0.08, 0.10, 0.92)
|
|
border.color: Qt.rgba(1.0, 1.0, 1.0, 0.18)
|
|
border.width: 1
|
|
|
|
Behavior on opacity {
|
|
NumberAnimation { duration: 120 }
|
|
}
|
|
|
|
Text {
|
|
id: tooltipText
|
|
anchors.centerIn: parent
|
|
text: itemRoot.name
|
|
font.family: Theme.fontFamily
|
|
font.pixelSize: Theme.fontSizeSmall
|
|
font.weight: Font.Medium
|
|
color: "#FFFFFF"
|
|
}
|
|
}
|
|
|
|
// Mouse interaction
|
|
MouseArea {
|
|
id: itemArea
|
|
anchors.fill: parent
|
|
hoverEnabled: true
|
|
cursorShape: Qt.PointingHandCursor
|
|
|
|
onClicked: {
|
|
bounceAnim.start();
|
|
if (itemRoot.execCmd !== "") {
|
|
GlobalState.launchApp(itemRoot.execCmd);
|
|
} else {
|
|
itemRoot.customClicked();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|