forked from Shinonome/dots-hyprland
Merge branch 'main' into background-clock-setting
This commit is contained in:
@@ -29,7 +29,7 @@ Variants {
|
||||
|
||||
// Hide when fullscreen
|
||||
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace=>workspace.monitor && workspace.monitor.name == monitor.name)
|
||||
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace=>((workspace.toplevels.values.filter(window=>window.wayland.fullscreen)[0] != undefined) && workspace.active))[0]
|
||||
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace=>((workspace.toplevels.values.filter(window=>window.wayland?.fullscreen)[0] != undefined) && workspace.active))[0]
|
||||
visible: GlobalStates.screenLocked || (!(activeWorkspaceWithFullscreen != undefined)) || !Config?.options.background.hideWhenFullscreen
|
||||
|
||||
// Workspaces
|
||||
@@ -278,9 +278,9 @@ Variants {
|
||||
}
|
||||
color: bgRoot.colText
|
||||
style: Text.Raised
|
||||
visible: Config.options.background.mantra !== ""
|
||||
visible: Config.options.background.quote !== ""
|
||||
styleColor: Appearance.colors.colShadow
|
||||
text: Config.options.background.mantra
|
||||
text: Config.options.background.quote
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import "./weather"
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Services.UPower
|
||||
import qs
|
||||
import qs.services
|
||||
@@ -307,10 +306,7 @@ Item { // Bar content region
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
MaterialSymbol {
|
||||
readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled
|
||||
readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected)
|
||||
readonly property bool bluetoothConnected: bluetoothDevice !== undefined
|
||||
text: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
|
||||
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
|
||||
@@ -216,8 +216,11 @@ Item {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
|
||||
text: `${button.workspaceValue}`
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
|
||||
family: Config.options?.bar.workspaces.useNerdFont ? Appearance.font.family.iconNerd : Appearance.font.family.main
|
||||
}
|
||||
text: Config.options?.bar.workspaces.numberMap[button.workspaceValue - 1] || button.workspaceValue
|
||||
elide: Text.ElideRight
|
||||
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
|
||||
Appearance.m3colors.m3onPrimary :
|
||||
|
||||
@@ -350,6 +350,10 @@ Singleton {
|
||||
property real baseVerticalBarWidth: 46
|
||||
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
|
||||
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
|
||||
property real wallpaperSelectorWidth: 1200
|
||||
property real wallpaperSelectorHeight: 690
|
||||
property real wallpaperSelectorItemMargins: 8
|
||||
property real wallpaperSelectorItemPadding: 6
|
||||
}
|
||||
|
||||
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
|
||||
|
||||
@@ -128,7 +128,7 @@ Singleton {
|
||||
property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
|
||||
property bool enableSidebar: true
|
||||
}
|
||||
property string mantra: ""
|
||||
property string quote: ""
|
||||
property bool hideWhenFullscreen: true
|
||||
}
|
||||
|
||||
@@ -175,6 +175,8 @@ Singleton {
|
||||
property bool showAppIcons: true
|
||||
property bool alwaysShowNumbers: false
|
||||
property int showNumberDelay: 300 // milliseconds
|
||||
property list<string> numberMap: ["1", "2"] // Characters to show instead of numbers on workspace indicator
|
||||
property bool useNerdFont: false
|
||||
}
|
||||
property JsonObject weather: JsonObject {
|
||||
property bool enable: false
|
||||
|
||||
@@ -8,12 +8,17 @@ import Quickshell
|
||||
|
||||
Singleton {
|
||||
// XDG Dirs, with "file://"
|
||||
readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
|
||||
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
|
||||
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
|
||||
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
||||
readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
|
||||
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
|
||||
|
||||
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
readonly property string music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[0]
|
||||
readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
|
||||
|
||||
// Other dirs used by the shell, without "file://"
|
||||
property string assetsPath: Quickshell.shellPath("assets")
|
||||
property string scriptPath: Quickshell.shellPath("scripts")
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
pragma Singleton
|
||||
|
||||
// From https://github.com/caelestia-dots/shell (GPLv3)
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getBluetoothDeviceMaterialSymbol(systemIconName: string): string {
|
||||
if (systemIconName.includes("headset") || systemIconName.includes("headphones"))
|
||||
return "headphones";
|
||||
if (systemIconName.includes("audio"))
|
||||
return "speaker";
|
||||
if (systemIconName.includes("phone"))
|
||||
return "smartphone";
|
||||
if (systemIconName.includes("mouse"))
|
||||
return "mouse";
|
||||
if (systemIconName.includes("keyboard"))
|
||||
return "keyboard";
|
||||
return "bluetooth";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
pragma Singleton
|
||||
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
// Formats
|
||||
readonly property list<string> validImageTypes: ["jpeg", "png", "webp", "tiff", "svg"]
|
||||
readonly property list<string> validImageExtensions: ["jpg", "jpeg", "png", "webp", "tif", "tiff", "svg"]
|
||||
|
||||
function isValidImageByName(name: string): bool {
|
||||
return validImageExtensions.some(t => name.endsWith(`.${t}`));
|
||||
}
|
||||
|
||||
// Thumbnails
|
||||
// https://specifications.freedesktop.org/thumbnail-spec/latest/directory.html
|
||||
readonly property var thumbnailSizes: ({
|
||||
"normal": 128,
|
||||
"large": 256,
|
||||
"x-large": 512,
|
||||
"xx-large": 1024
|
||||
})
|
||||
function thumbnailSizeNameForDimensions(width: int, height: int): string {
|
||||
const sizeNames = Object.keys(thumbnailSizes);
|
||||
for(let i = 0; i < sizeNames.length; i++) {
|
||||
const sizeName = sizeNames[i];
|
||||
const maxSize = thumbnailSizes[sizeName];
|
||||
if (width <= maxSize && height <= maxSize) return sizeName;
|
||||
}
|
||||
return "xx-large";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
/**
|
||||
* Thumbnail image. It currently generates to the right place at the right size, but does not handle metadata/maintenance on modification.
|
||||
* See Freedesktop's spec: https://specifications.freedesktop.org/thumbnail-spec/thumbnail-spec-latest.html
|
||||
*/
|
||||
Image {
|
||||
id: root
|
||||
|
||||
property bool generateThumbnail: true
|
||||
required property string sourcePath
|
||||
property string thumbnailSizeName: Images.thumbnailSizeNameForDimensions(sourceSize.width, sourceSize.height)
|
||||
property string thumbnailPath: {
|
||||
if (sourcePath.length == 0) return;
|
||||
const resolvedUrlWithoutFileProtocol = FileUtils.trimFileProtocol(`${Qt.resolvedUrl(sourcePath)}`);
|
||||
const encodedUrlWithoutFileProtocol = resolvedUrlWithoutFileProtocol.split("/").map(part => encodeURIComponent(part)).join("/");
|
||||
const md5Hash = Qt.md5(`file://${encodedUrlWithoutFileProtocol}`);
|
||||
return `${Directories.genericCache}/thumbnails/${thumbnailSizeName}/${md5Hash}.png`;
|
||||
}
|
||||
source: thumbnailPath
|
||||
|
||||
asynchronous: true
|
||||
cache: false
|
||||
smooth: true
|
||||
mipmap: false
|
||||
|
||||
opacity: status === Image.Ready ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
onSourceSizeChanged: {
|
||||
if (!root.generateThumbnail) return;
|
||||
thumbnailGeneration.running = false;
|
||||
thumbnailGeneration.running = true;
|
||||
}
|
||||
Process {
|
||||
id: thumbnailGeneration
|
||||
command: {
|
||||
const maxSize = Images.thumbnailSizes[root.thumbnailSizeName];
|
||||
return ["bash", "-c",
|
||||
`[ -f '${FileUtils.trimFileProtocol(root.thumbnailPath)}' ] && exit 0 || { magick '${root.sourcePath}' -resize ${maxSize}x${maxSize} '${FileUtils.trimFileProtocol(root.thumbnailPath)}' && exit 1; }`
|
||||
]
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 1) { // Force reload if thumbnail had to be generated
|
||||
root.source = "";
|
||||
root.source = root.thumbnailPath; // Force reload
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,20 @@ Singleton {
|
||||
return trimmed.split(/[\\/]/).pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the folder name from a directory path
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function folderNameForPath(str) {
|
||||
if (typeof str !== "string") return "";
|
||||
const trimmed = trimFileProtocol(str);
|
||||
// Remove trailing slash if present
|
||||
const noTrailing = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
||||
if (!noTrailing) return "";
|
||||
return noTrailing.split(/[\\/]/).pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file extension from a file path or name
|
||||
* @param {string} str
|
||||
@@ -38,4 +52,18 @@ Singleton {
|
||||
}
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the parent directory of a given file path
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function parentDirectory(str) {
|
||||
if (typeof str !== "string") return "";
|
||||
const trimmed = trimFileProtocol(str);
|
||||
const parts = trimmed.split(/[\\/]/);
|
||||
if (parts.length <= 1) return "";
|
||||
parts.pop();
|
||||
return parts.join("/");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -217,4 +217,8 @@ Singleton {
|
||||
return str;
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function cleanCliphistEntry(str: string): string {
|
||||
return str.replace(/^\d+\t/, "");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
required property var directory
|
||||
property bool showBreadcrumb: true
|
||||
onShowBreadcrumbChanged: {
|
||||
addressInput.text = root.directory;
|
||||
}
|
||||
|
||||
signal navigateToDirectory(string path)
|
||||
|
||||
property real padding: 6
|
||||
implicitWidth: mainLayout.implicitWidth + padding * 2
|
||||
implicitHeight: mainLayout.implicitHeight + padding * 2
|
||||
color: Appearance.colors.colLayer2
|
||||
|
||||
function focusBreadcrumb() {
|
||||
root.showBreadcrumb = false;
|
||||
addressInput.forceActiveFocus();
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: mainLayout
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.padding
|
||||
}
|
||||
spacing: 8
|
||||
|
||||
RippleButton {
|
||||
id: parentDirButton
|
||||
onClicked: root.navigateToDirectory(FileUtils.parentDirectory(root.directory))
|
||||
contentItem: MaterialSymbol {
|
||||
text: "drive_folder_upload"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
Rectangle {
|
||||
id: directoryEntry
|
||||
visible: !root.showBreadcrumb
|
||||
anchors.fill: parent
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: Appearance.rounding.full
|
||||
implicitWidth: addressInput.implicitWidth
|
||||
implicitHeight: addressInput.implicitHeight
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (directoryEntry.visible && event.key === Qt.Key_Escape) {
|
||||
root.showBreadcrumb = true;
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
|
||||
StyledTextInput {
|
||||
id: addressInput
|
||||
anchors.fill: parent
|
||||
padding: 10
|
||||
text: root.directory
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
root.navigateToDirectory(text);
|
||||
root.showBreadcrumb = true;
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
// I-beam cursor
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: breadcrumbLoader
|
||||
active: root.showBreadcrumb
|
||||
visible: root.showBreadcrumb
|
||||
anchors.fill: parent
|
||||
sourceComponent: AddressBreadcrumb {
|
||||
directory: root.directory
|
||||
onNavigateToDirectory: dir => {
|
||||
root.navigateToDirectory(dir);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RippleButton {
|
||||
id: dirEditButton
|
||||
toggled: !root.showBreadcrumb
|
||||
onClicked: root.showBreadcrumb = !root.showBreadcrumb
|
||||
contentItem: MaterialSymbol {
|
||||
text: "edit"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Edit directory")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
ListView {
|
||||
id: root
|
||||
required property var directory
|
||||
property var breadcrumbDirectory: ""
|
||||
Component.onCompleted: breadcrumbDirectory = directory;
|
||||
onDirectoryChanged: {
|
||||
if (breadcrumbDirectory.startsWith(directory)) return;
|
||||
breadcrumbDirectory = directory
|
||||
}
|
||||
|
||||
signal navigateToDirectory(string path)
|
||||
|
||||
orientation: ListView.Horizontal
|
||||
clip: true
|
||||
spacing: 2
|
||||
|
||||
model: breadcrumbDirectory.split("/")
|
||||
delegate: SelectionGroupButton {
|
||||
id: folderButton
|
||||
required property var modelData
|
||||
required property int index
|
||||
buttonText: index === 0 ? "/" : modelData
|
||||
toggled: {
|
||||
if (directory.trim() === "/") return index === 0;
|
||||
return index === directory.split("/").length - 1
|
||||
}
|
||||
leftmost: index === 0
|
||||
rightmost: index === breadcrumbDirectory.split("/").length - 1
|
||||
|
||||
onClicked: {
|
||||
root.navigateToDirectory(breadcrumbDirectory.split("/").slice(0, index + 1).join("/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,29 +1,36 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
|
||||
*/
|
||||
RippleButton {
|
||||
id: button
|
||||
id: root
|
||||
|
||||
property string buttonText
|
||||
implicitHeight: 30
|
||||
implicitWidth: buttonTextWidget.implicitWidth + 15 * 2
|
||||
padding: 14
|
||||
implicitHeight: 36
|
||||
implicitWidth: buttonTextWidget.implicitWidth + padding * 2
|
||||
buttonRadius: Appearance?.rounding.full ?? 9999
|
||||
|
||||
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
property alias colText: buttonTextWidget.color
|
||||
|
||||
contentItem: StyledText {
|
||||
id: buttonTextWidget
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 15
|
||||
anchors.rightMargin: 15
|
||||
anchors.leftMargin: root.padding
|
||||
anchors.rightMargin: root.padding
|
||||
text: buttonText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance?.font.pixelSize.small ?? 12
|
||||
color: button.enabled ? button.colEnabled : button.colDisabled
|
||||
color: root.enabled ? root.colEnabled : root.colDisabled
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property bool active: false
|
||||
|
||||
horizontalPadding: Appearance.rounding.large
|
||||
verticalPadding: 12
|
||||
|
||||
clip: true
|
||||
pointingHandCursor: !active
|
||||
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: contentItem.implicitHeight + verticalPadding * 2
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
buttonRadius: 0
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
// From https://github.com/caelestia-dots/shell with modifications.
|
||||
// License: GPLv3
|
||||
|
||||
Image {
|
||||
id: root
|
||||
required property var fileModelData
|
||||
asynchronous: true
|
||||
fillMode: Image.PreserveAspectFit
|
||||
|
||||
source: {
|
||||
if (!fileModelData.fileIsDir)
|
||||
return Quickshell.iconPath("application-x-zerosize");
|
||||
|
||||
if ([Directories.documents, Directories.downloads, Directories.music, Directories.pictures, Directories.videos].some(dir => FileUtils.trimFileProtocol(dir) === fileModelData.filePath))
|
||||
return Quickshell.iconPath(`folder-${fileModelData.fileName.toLowerCase()}`);
|
||||
|
||||
return Quickshell.iconPath("inode-directory");
|
||||
}
|
||||
|
||||
onStatusChanged: {
|
||||
if (status === Image.Error)
|
||||
source = Quickshell.iconPath("error");
|
||||
}
|
||||
|
||||
Process {
|
||||
running: !fileModelData.fileIsDir
|
||||
command: ["file", "--mime", "-b", fileModelData.filePath]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
const mime = text.split(";")[0].replace("/", "-");
|
||||
root.source = Images.validImageTypes.some(t => mime === `image-${t}`) ? fileModelData.fileUrl : Quickshell.iconPath(mime, "image-missing");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled TextArea (filled style)
|
||||
* https://m3.material.io/components/text-fields/overview
|
||||
* Note: We don't use NativeRendering because it makes the small placeholder text look weird
|
||||
*/
|
||||
TextArea {
|
||||
id: root
|
||||
Material.theme: Material.System
|
||||
Material.accent: Appearance.m3colors.m3primary
|
||||
Material.primary: Appearance.m3colors.m3primary
|
||||
Material.background: Appearance.m3colors.m3surface
|
||||
Material.foreground: Appearance.m3colors.m3onSurface
|
||||
Material.containerStyle: Material.Filled
|
||||
renderType: Text.QtRendering
|
||||
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
|
||||
background: Rectangle {
|
||||
implicitHeight: 56
|
||||
color: Appearance.m3colors.m3surface
|
||||
topLeftRadius: 4
|
||||
topRightRadius: 4
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
color: root.focus ? Appearance.m3colors.m3primary :
|
||||
root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
pixelSize: Appearance?.font.pixelSize.small ?? 15
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
}
|
||||
@@ -4,44 +4,24 @@ import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
/**
|
||||
* Material 3 styled TextArea (filled style)
|
||||
* Material 3 styled TextField (filled style)
|
||||
* https://m3.material.io/components/text-fields/overview
|
||||
* Note: We don't use NativeRendering because it makes the small placeholder text look weird
|
||||
*/
|
||||
TextArea {
|
||||
TextField {
|
||||
id: root
|
||||
Material.theme: Material.System
|
||||
Material.accent: Appearance.m3colors.m3primary
|
||||
Material.primary: Appearance.m3colors.m3primary
|
||||
Material.background: Appearance.m3colors.m3surface
|
||||
Material.foreground: Appearance.m3colors.m3onSurface
|
||||
Material.containerStyle: Material.Filled
|
||||
Material.containerStyle: Material.Outlined
|
||||
renderType: Text.QtRendering
|
||||
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
placeholderTextColor: Appearance.m3colors.m3outline
|
||||
|
||||
background: Rectangle {
|
||||
implicitHeight: 56
|
||||
color: Appearance.m3colors.m3surface
|
||||
topLeftRadius: 4
|
||||
topRightRadius: 4
|
||||
Rectangle {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
bottom: parent.bottom
|
||||
}
|
||||
height: 1
|
||||
color: root.focus ? Appearance.m3colors.m3primary :
|
||||
root.hovered ? Appearance.m3colors.m3outline : Appearance.m3colors.m3outlineVariant
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
clip: true
|
||||
|
||||
font {
|
||||
family: Appearance?.font.family.main ?? "sans-serif"
|
||||
@@ -49,4 +29,11 @@ TextArea {
|
||||
hintingPreference: Font.PreferFullHinting
|
||||
}
|
||||
wrapMode: TextEdit.Wrap
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.NoButton
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.IBeamCursor
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,5 +3,5 @@ import QtQuick
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onPressed: (mouse) => mouse.accepted = false
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
@@ -12,6 +12,7 @@ Button {
|
||||
id: root
|
||||
property bool toggled
|
||||
property string buttonText
|
||||
property bool pointingHandCursor: true
|
||||
property real buttonRadius: Appearance?.rounding?.small ?? 4
|
||||
property real buttonRadiusPressed: buttonRadius
|
||||
property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
@@ -58,7 +59,7 @@ Button {
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onPressed: (event) => {
|
||||
if(event.button === Qt.RightButton) {
|
||||
|
||||
@@ -5,7 +5,7 @@ Item {
|
||||
id: root
|
||||
|
||||
enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight }
|
||||
property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft
|
||||
property var corner: RoundCorner.CornerEnum.TopLeft
|
||||
|
||||
property int implicitSize: 25
|
||||
property color color: "#000000"
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Controls.Material
|
||||
import QtQuick.Controls
|
||||
|
||||
ProgressBar {
|
||||
indeterminate: true
|
||||
Material.accent: Appearance.colors.colPrimary
|
||||
}
|
||||
@@ -14,6 +14,8 @@ ListView {
|
||||
property int dragIndex: -1
|
||||
property real dragDistance: 0
|
||||
property bool popin: true
|
||||
property bool animateAppearance: true
|
||||
property bool animateMovement: false
|
||||
// Accumulated scroll destination so wheel deltas stack while animating
|
||||
property real scrollTargetY: 0
|
||||
|
||||
@@ -66,17 +68,17 @@ ListView {
|
||||
}
|
||||
|
||||
add: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
from: 0,
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
] : []
|
||||
}
|
||||
|
||||
addDisplaced: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
@@ -84,46 +86,46 @@ ListView {
|
||||
properties: popin ? "opacity,scale" : "opacity",
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
] : []
|
||||
}
|
||||
|
||||
// displaced: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
displaced: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
// move: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
// moveDisplaced: Transition {
|
||||
// animations: [
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// property: "y",
|
||||
// }),
|
||||
// Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
// properties: "opacity,scale",
|
||||
// to: 1,
|
||||
// }),
|
||||
// ]
|
||||
// }
|
||||
move: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
moveDisplaced: Transition {
|
||||
animations: root.animateMovement ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
] : []
|
||||
}
|
||||
|
||||
remove: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "x",
|
||||
to: root.width + root.removeOvershoot,
|
||||
@@ -132,12 +134,12 @@ ListView {
|
||||
property: "opacity",
|
||||
to: 0,
|
||||
})
|
||||
]
|
||||
] : []
|
||||
}
|
||||
|
||||
// This is movement when something is removed, not removing animation!
|
||||
removeDisplaced: Transition {
|
||||
animations: [
|
||||
animations: animateAppearance ? [
|
||||
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
|
||||
property: "y",
|
||||
}),
|
||||
@@ -145,6 +147,6 @@ ListView {
|
||||
properties: "opacity,scale",
|
||||
to: 1,
|
||||
}),
|
||||
]
|
||||
] : []
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
ScrollBar {
|
||||
id: root
|
||||
|
||||
policy: ScrollBar.AsNeeded
|
||||
|
||||
contentItem: Rectangle {
|
||||
implicitWidth: 4
|
||||
implicitHeight: root.visualSize
|
||||
radius: width / 2
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
|
||||
opacity: root.policy === ScrollBar.AlwaysOn || (root.active && root.size < 1.0) ? 0.5 : 0
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 350
|
||||
easing.type: Appearance.animation.elementMoveFast.type
|
||||
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import QtQuick.Controls
|
||||
* Does not include visual layout, but includes the easily neglected colors.
|
||||
*/
|
||||
TextInput {
|
||||
color: Appearance.colors.colOnLayer1
|
||||
renderType: Text.NativeRendering
|
||||
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
|
||||
selectionColor: Appearance.colors.colSecondaryContainer
|
||||
|
||||
@@ -10,7 +10,7 @@ import qs.modules.common.widgets
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property real padding: 6
|
||||
property real padding: 8
|
||||
property alias colBackground: background.color
|
||||
default property alias data: toolbarLayout.data
|
||||
implicitWidth: background.implicitWidth
|
||||
@@ -23,13 +23,14 @@ Item {
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colLayer2
|
||||
color: Appearance.m3colors.m3surfaceContainer // Needs to be opaque
|
||||
implicitHeight: toolbarLayout.implicitHeight + root.padding * 2
|
||||
implicitWidth: toolbarLayout.implicitWidth + root.padding * 2
|
||||
radius: Appearance.rounding.full
|
||||
|
||||
RowLayout {
|
||||
id: toolbarLayout
|
||||
spacing: 4
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: root.padding
|
||||
|
||||
@@ -4,7 +4,5 @@ import qs.modules.common
|
||||
|
||||
RippleButton {
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: 2
|
||||
Layout.bottomMargin: 2
|
||||
buttonRadius: Appearance.rounding.full
|
||||
}
|
||||
|
||||
@@ -10,8 +10,6 @@ TextField {
|
||||
property alias colBackground: background.color
|
||||
|
||||
Layout.fillHeight: true
|
||||
Layout.topMargin: 2
|
||||
Layout.bottomMargin: 2
|
||||
implicitWidth: 200
|
||||
padding: 10
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool show: false
|
||||
default property alias data: contentColumn.data
|
||||
property real backgroundHeight: 600
|
||||
property real backgroundAnimationMovementDistance: 60
|
||||
|
||||
signal dismiss()
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dismiss();
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
color: root.show ? Appearance.colors.colScrim : ColorUtils.transparentize(Appearance.colors.colScrim)
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
visible: dialogBackground.implicitHeight > 0
|
||||
|
||||
onShowChanged: {
|
||||
dialogBackgroundHeightAnimation.easing.bezierCurve = (show ? Appearance.animationCurves.emphasizedDecel : Appearance.animationCurves.emphasizedAccel)
|
||||
dialogBackground.implicitHeight = show ? backgroundHeight : 0
|
||||
}
|
||||
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
MouseArea { // Clicking outside the dialog should dismiss
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
onPressed: root.dismiss()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: dialogBackground
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
radius: Appearance.rounding.large
|
||||
color: Appearance.m3colors.m3surfaceContainerHigh // Use opaque version of layer3
|
||||
|
||||
property real targetY: root.height / 2 - root.backgroundHeight / 2
|
||||
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
|
||||
implicitWidth: 350
|
||||
implicitHeight: 0
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
id: dialogBackgroundHeightAnimation
|
||||
duration: Appearance.animation.elementMoveFast.duration
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.emphasizedDecel
|
||||
}
|
||||
}
|
||||
Behavior on y {
|
||||
NumberAnimation {
|
||||
duration: dialogBackgroundHeightAnimation.duration
|
||||
easing.type: dialogBackgroundHeightAnimation.easing.type
|
||||
easing.bezierCurve: dialogBackgroundHeightAnimation.easing.bezierCurve
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea { // So clicking inside the dialog won't dismiss
|
||||
anchors.fill: parent
|
||||
acceptedButtons: Qt.AllButtons
|
||||
hoverEnabled: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: dialogBackground.radius
|
||||
}
|
||||
spacing: 16
|
||||
opacity: root.show ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: 4
|
||||
|
||||
// These shouldn't be needed but it would be a terrible waste of space to follow the spec
|
||||
Layout.margins: -8
|
||||
Layout.topMargin: 0
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
implicitHeight: 1
|
||||
color: Appearance.colors.colOutline
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
Layout.topMargin: -8
|
||||
Layout.bottomMargin: -8
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledText {
|
||||
text: "Dialog Title"
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.title
|
||||
family: Appearance.font.family.title
|
||||
}
|
||||
}
|
||||
@@ -33,8 +33,6 @@ Scope {
|
||||
}
|
||||
|
||||
function tryUnlock() {
|
||||
if (currentText === "") return;
|
||||
|
||||
root.unlockInProgress = true;
|
||||
pam.start();
|
||||
}
|
||||
@@ -42,12 +40,6 @@ Scope {
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
// Its best to have a custom pam config for quickshell, as the system one
|
||||
// might not be what your interface expects, and break in some way.
|
||||
// This particular example only supports passwords.
|
||||
configDirectory: "pam"
|
||||
config: "password.conf"
|
||||
|
||||
// pam_unix will ask for a response for the password prompt
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
|
||||
@@ -133,7 +133,7 @@ MouseArea {
|
||||
id: confirmButton
|
||||
implicitWidth: height
|
||||
toggled: true
|
||||
enabled: !root.context.unlockInProgress && root.context.currentText.length > 0
|
||||
enabled: !root.context.unlockInProgress
|
||||
colBackgroundToggled: Appearance.colors.colPrimary
|
||||
|
||||
onClicked: root.context.tryUnlock()
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
auth required pam_unix.so
|
||||
@@ -40,37 +40,34 @@ Scope {
|
||||
}
|
||||
return (
|
||||
// Remove unecessary native buses from browsers if there's plasma integration
|
||||
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) &&
|
||||
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
|
||||
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
|
||||
// playerctld just copies other buses and we don't need duplicates
|
||||
!player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') &&
|
||||
// Non-instance mpd bus
|
||||
!(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd'))
|
||||
);
|
||||
!(player.dbusName?.endsWith('.mpd') && !player.dbusName.endsWith('MediaPlayer2.mpd')));
|
||||
}
|
||||
function filterDuplicatePlayers(players) {
|
||||
let filtered = [];
|
||||
let used = new Set();
|
||||
|
||||
for (let i = 0; i < players.length; ++i) {
|
||||
if (used.has(i)) continue;
|
||||
if (used.has(i))
|
||||
continue;
|
||||
let p1 = players[i];
|
||||
let group = [i];
|
||||
|
||||
// Find duplicates by trackTitle prefix
|
||||
for (let j = i + 1; j < players.length; ++j) {
|
||||
let p2 = players[j];
|
||||
if (p1.trackTitle && p2.trackTitle &&
|
||||
(p1.trackTitle.includes(p2.trackTitle)
|
||||
|| p2.trackTitle.includes(p1.trackTitle))
|
||||
|| (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) {
|
||||
if (p1.trackTitle && p2.trackTitle && (p1.trackTitle.includes(p2.trackTitle) || p2.trackTitle.includes(p1.trackTitle)) || (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) {
|
||||
group.push(j);
|
||||
}
|
||||
}
|
||||
|
||||
// Pick the one with non-empty trackArtUrl, or fallback to the first
|
||||
let chosenIdx = group.find(idx => players[idx].trackArtUrl && players[idx].trackArtUrl.length > 0);
|
||||
if (chosenIdx === undefined) chosenIdx = group[0];
|
||||
if (chosenIdx === undefined)
|
||||
chosenIdx = group[0];
|
||||
|
||||
filtered.push(players[chosenIdx]);
|
||||
group.forEach(idx => used.add(idx));
|
||||
@@ -133,6 +130,16 @@ Scope {
|
||||
item: playerColumnLayout
|
||||
}
|
||||
|
||||
HyprlandFocusGrab {
|
||||
windows: [mediaControlsRoot]
|
||||
active: mediaControlsLoader.active
|
||||
onCleared: () => {
|
||||
if (!active) {
|
||||
GlobalStates.mediaControlsOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: playerColumnLayout
|
||||
anchors.fill: parent
|
||||
@@ -148,6 +155,43 @@ Scope {
|
||||
visualizerPoints: root.visualizerPoints
|
||||
implicitWidth: widgetWidth
|
||||
implicitHeight: widgetHeight
|
||||
radius: root.popupRounding
|
||||
}
|
||||
}
|
||||
|
||||
Item { // No player placeholder
|
||||
Layout.fillWidth: true
|
||||
visible: root.meaningfulPlayers.length === 0
|
||||
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
|
||||
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: placeholderBackground
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: placeholderBackground
|
||||
anchors.centerIn: parent
|
||||
color: Appearance.colors.colLayer0
|
||||
radius: root.popupRounding
|
||||
property real padding: 20
|
||||
implicitWidth: placeholderLayout.implicitWidth + padding * 2
|
||||
implicitHeight: placeholderLayout.implicitHeight + padding * 2
|
||||
|
||||
ColumnLayout {
|
||||
id: placeholderLayout
|
||||
anchors.centerIn: parent
|
||||
|
||||
StyledText {
|
||||
text: Translation.tr("No active player")
|
||||
font.pixelSize: Appearance.font.pixelSize.large
|
||||
}
|
||||
StyledText {
|
||||
color: Appearance.colors.colSubtext
|
||||
text: Translation.tr("Make sure your player has MPRIS support\nor try turning off duplicate player filtering")
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -159,7 +203,8 @@ Scope {
|
||||
|
||||
function toggle(): void {
|
||||
mediaControlsLoader.active = !mediaControlsLoader.active;
|
||||
if(mediaControlsLoader.active) Notifications.timeoutAll();
|
||||
if (mediaControlsLoader.active)
|
||||
Notifications.timeoutAll();
|
||||
}
|
||||
|
||||
function close(): void {
|
||||
@@ -196,5 +241,4 @@ Scope {
|
||||
GlobalStates.mediaControlsOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ Item { // Player instance
|
||||
property list<real> visualizerPoints: []
|
||||
property real maxVisualizerValue: 1000 // Max value in the data points
|
||||
property int visualizerSmoothing: 2 // Number of points to average for smoothing
|
||||
property real radius
|
||||
|
||||
component TrackChangeButton: RippleButton {
|
||||
implicitWidth: 24
|
||||
@@ -107,7 +108,7 @@ Item { // Player instance
|
||||
anchors.fill: parent
|
||||
anchors.margins: Appearance.sizes.elevationMargin
|
||||
color: blendedColors.colLayer0
|
||||
radius: root.popupRounding
|
||||
radius: playerController.radius
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
@@ -141,7 +142,7 @@ Item { // Player instance
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3)
|
||||
radius: root.popupRounding
|
||||
radius: playerController.radius
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -296,7 +296,7 @@ Item { // Wrapper
|
||||
return Cliphist.fuzzyQuery(searchString).map(entry => {
|
||||
return {
|
||||
cliphistRawString: entry,
|
||||
name: entry.replace(/^\s*\S+\s+/, ""),
|
||||
name: StringUtils.cleanCliphistEntry(entry),
|
||||
clickActionName: "",
|
||||
type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`,
|
||||
execute: () => {
|
||||
|
||||
@@ -126,7 +126,7 @@ Scope {
|
||||
|
||||
// Hide when fullscreen
|
||||
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace => workspace.monitor && workspace.monitor.name == monitor.name)
|
||||
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace => ((workspace.toplevels.values.filter(window => window.wayland.fullscreen)[0] != undefined) && workspace.active))[0]
|
||||
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace => ((workspace.toplevels.values.filter(window => window.wayland?.fullscreen)[0] != undefined) && workspace.active))[0]
|
||||
property bool fullscreen: activeWorkspaceWithFullscreen != undefined
|
||||
|
||||
CornerPanelWindow {
|
||||
|
||||
@@ -49,7 +49,7 @@ ContentPage {
|
||||
}
|
||||
ContentSection {
|
||||
title: Translation.tr("AI")
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("System prompt")
|
||||
text: Config.options.ai.systemPrompt
|
||||
@@ -115,7 +115,7 @@ ContentPage {
|
||||
|
||||
ContentSection {
|
||||
title: Translation.tr("Networking")
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("User agent (for services that require it)")
|
||||
text: Config.options.networking.userAgent
|
||||
@@ -159,7 +159,7 @@ ContentPage {
|
||||
ConfigRow {
|
||||
uniform: true
|
||||
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Action")
|
||||
text: Config.options.search.prefix.action
|
||||
@@ -168,7 +168,7 @@ ContentPage {
|
||||
Config.options.search.prefix.action = text;
|
||||
}
|
||||
}
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Clipboard")
|
||||
text: Config.options.search.prefix.clipboard
|
||||
@@ -177,7 +177,7 @@ ContentPage {
|
||||
Config.options.search.prefix.clipboard = text;
|
||||
}
|
||||
}
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Emojis")
|
||||
text: Config.options.search.prefix.emojis
|
||||
@@ -190,7 +190,7 @@ ContentPage {
|
||||
}
|
||||
ContentSubsection {
|
||||
title: Translation.tr("Web search")
|
||||
MaterialTextField {
|
||||
MaterialTextArea {
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Base URL")
|
||||
text: Config.options.search.engineBaseUrl
|
||||
|
||||
@@ -15,7 +15,10 @@ Rectangle {
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
property int selectedTab: 0
|
||||
property var tabButtonList: [{"icon": "notifications", "name": Translation.tr("Notifications")}, {"icon": "volume_up", "name": Translation.tr("Audio")}]
|
||||
property var tabButtonList: [
|
||||
{"icon": "notifications", "name": Translation.tr("Notifications")},
|
||||
{"icon": "volume_up", "name": Translation.tr("Audio")}
|
||||
]
|
||||
|
||||
Keys.onPressed: (event) => {
|
||||
if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "./quickToggles/"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
@@ -16,8 +15,6 @@ import Quickshell.Hyprland
|
||||
Scope {
|
||||
id: root
|
||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||
property int sidebarPadding: 12
|
||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||
|
||||
PanelWindow {
|
||||
id: sidebarRoot
|
||||
@@ -67,124 +64,7 @@ Scope {
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: Item {
|
||||
implicitHeight: sidebarRightBackground.implicitHeight
|
||||
implicitWidth: sidebarRightBackground.implicitWidth
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: sidebarRightBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: sidebarRightBackground
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
|
||||
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: sidebarPadding
|
||||
spacing: sidebarPadding
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: false
|
||||
spacing: 10
|
||||
Layout.margins: 10
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 0
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Hyprland.dispatch("reload")
|
||||
Quickshell.reload(true)
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Reload Hyprland & Quickshell")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "settings"
|
||||
onClicked: {
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath])
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Settings")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "power_settings_new"
|
||||
onClicked: {
|
||||
GlobalStates.sessionOpen = true
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Session")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 5
|
||||
padding: 5
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
NetworkToggle {}
|
||||
BluetoothToggle {}
|
||||
NightLight {}
|
||||
GameMode {}
|
||||
IdleInhibitor {}
|
||||
EasyEffectsToggle {}
|
||||
CloudflareWarp {}
|
||||
}
|
||||
|
||||
// Center widget group
|
||||
CenterWidgetGroup {
|
||||
focus: sidebarRoot.visible
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
BottomWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
sourceComponent: SidebarRightContent {}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import "./quickToggles/"
|
||||
import "./wifiNetworks/"
|
||||
import "./bluetoothDevices/"
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||
property int sidebarPadding: 12
|
||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||
property bool showWifiDialog: false
|
||||
property bool showBluetoothDialog: false
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onSidebarRightOpenChanged() {
|
||||
if (!GlobalStates.sidebarRightOpen) {
|
||||
root.showWifiDialog = false;
|
||||
root.showBluetoothDialog = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: sidebarRightBackground.implicitHeight
|
||||
implicitWidth: sidebarRightBackground.implicitWidth
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: sidebarRightBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: sidebarRightBackground
|
||||
|
||||
anchors.fill: parent
|
||||
implicitHeight: parent.height - Appearance.sizes.hyprlandGapsOut * 2
|
||||
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
|
||||
color: Appearance.colors.colLayer0
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
anchors.margins: sidebarPadding
|
||||
spacing: sidebarPadding
|
||||
|
||||
RowLayout {
|
||||
Layout.fillHeight: false
|
||||
spacing: 10
|
||||
Layout.margins: 10
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 0
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "restart_alt"
|
||||
onClicked: {
|
||||
Hyprland.dispatch("reload");
|
||||
Quickshell.reload(true);
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Reload Hyprland & Quickshell")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "settings"
|
||||
onClicked: {
|
||||
GlobalStates.sidebarRightOpen = false;
|
||||
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]);
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Settings")
|
||||
}
|
||||
}
|
||||
QuickToggleButton {
|
||||
toggled: false
|
||||
buttonIcon: "power_settings_new"
|
||||
onClicked: {
|
||||
GlobalStates.sessionOpen = true;
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Session")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
spacing: 5
|
||||
padding: 5
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
NetworkToggle {
|
||||
altAction: () => {
|
||||
Network.enableWifi();
|
||||
Network.rescanWifi();
|
||||
root.showWifiDialog = true;
|
||||
}
|
||||
}
|
||||
BluetoothToggle {
|
||||
altAction: () => {
|
||||
Bluetooth.defaultAdapter.enabled = true;
|
||||
Bluetooth.defaultAdapter.discovering = true;
|
||||
root.showBluetoothDialog = true;
|
||||
}
|
||||
}
|
||||
NightLight {}
|
||||
GameMode {}
|
||||
IdleInhibitor {}
|
||||
EasyEffectsToggle {}
|
||||
CloudflareWarp {}
|
||||
}
|
||||
|
||||
CenterWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
BottomWidgetGroup {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
Layout.fillHeight: false
|
||||
Layout.fillWidth: true
|
||||
Layout.preferredHeight: implicitHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowWifiDialogChanged: if (showWifiDialog) wifiDialogLoader.active = true;
|
||||
Loader {
|
||||
id: wifiDialogLoader
|
||||
anchors.fill: parent
|
||||
|
||||
active: root.showWifiDialog || item.visible
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
item.show = true;
|
||||
item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: WifiDialog {
|
||||
onDismiss: {
|
||||
show = false
|
||||
root.showWifiDialog = false
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible && !root.showWifiDialog) wifiDialogLoader.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onShowBluetoothDialogChanged: {
|
||||
if (showBluetoothDialog) bluetoothDialogLoader.active = true;
|
||||
else Bluetooth.defaultAdapter.discovering = false;
|
||||
}
|
||||
Loader {
|
||||
id: bluetoothDialogLoader
|
||||
anchors.fill: parent
|
||||
|
||||
active: root.showBluetoothDialog || item.visible
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
item.show = true;
|
||||
item.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
|
||||
sourceComponent: BluetoothDialog {
|
||||
onDismiss: {
|
||||
show = false
|
||||
root.showBluetoothDialog = false
|
||||
}
|
||||
onVisibleChanged: {
|
||||
if (!visible && !root.showBluetoothDialog) bluetoothDialogLoader.active = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DialogListItem {
|
||||
id: root
|
||||
required property var device
|
||||
property bool expanded: false
|
||||
pointingHandCursor: !expanded
|
||||
|
||||
onClicked: expanded = !expanded
|
||||
altAction: () => expanded = !expanded
|
||||
|
||||
component ActionButton: DialogButton {
|
||||
colBackground: Appearance.colors.colPrimary
|
||||
colBackgroundHover: Appearance.colors.colPrimaryHover
|
||||
colRipple: Appearance.colors.colPrimaryActive
|
||||
colText: Appearance.colors.colOnPrimary
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.verticalPadding
|
||||
leftMargin: root.horizontalPadding
|
||||
rightMargin: root.horizontalPadding
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
// Name
|
||||
spacing: 10
|
||||
|
||||
MaterialSymbol {
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: Icons.getBluetoothDeviceMaterialSymbol(root.device?.icon || "")
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
spacing: 2
|
||||
Layout.fillWidth: true
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
text: root.device?.name || Translation.tr("Unknown device")
|
||||
}
|
||||
StyledText {
|
||||
visible: (root.device?.connected || root.device?.paired) ?? false
|
||||
Layout.fillWidth: true
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
color: Appearance.colors.colSubtext
|
||||
elide: Text.ElideRight
|
||||
text: {
|
||||
if (!root.device?.paired) return "";
|
||||
let statusText = root.device?.connected ? Translation.tr("Connected") : Translation.tr("Paired");
|
||||
if (!root.device?.batteryAvailable) return statusText;
|
||||
statusText += ` • ${Math.round(root.device?.battery * 100)}%`;
|
||||
return statusText;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MaterialSymbol {
|
||||
text: "keyboard_arrow_down"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colOnLayer3
|
||||
rotation: root.expanded ? 180 : 0
|
||||
Behavior on rotation {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
visible: root.expanded
|
||||
Layout.topMargin: 8
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
ActionButton {
|
||||
buttonText: root.device?.connected ? Translation.tr("Disconnect") : Translation.tr("Connect")
|
||||
|
||||
onClicked: {
|
||||
if (root.device?.connected) {
|
||||
root.device.disconnect();
|
||||
} else {
|
||||
root.device.connect();
|
||||
}
|
||||
}
|
||||
}
|
||||
ActionButton {
|
||||
visible: root.device?.paired ?? false
|
||||
colBackground: Appearance.colors.colError
|
||||
colBackgroundHover: Appearance.colors.colErrorHover
|
||||
colRipple: Appearance.colors.colErrorActive
|
||||
colText: Appearance.colors.colOnError
|
||||
|
||||
buttonText: Translation.tr("Forget")
|
||||
onClicked: {
|
||||
root.device?.forget();
|
||||
}
|
||||
}
|
||||
}
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,76 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
WindowDialog {
|
||||
id: root
|
||||
|
||||
WindowDialogTitle {
|
||||
text: Translation.tr("Bluetooth devices")
|
||||
}
|
||||
WindowDialogSeparator {
|
||||
visible: !(Bluetooth.defaultAdapter?.discovering ?? false)
|
||||
}
|
||||
StyledIndeterminateProgressBar {
|
||||
visible: Bluetooth.defaultAdapter?.discovering ?? false
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -8
|
||||
Layout.bottomMargin: -8
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
}
|
||||
StyledListView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -15
|
||||
Layout.bottomMargin: -16
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
|
||||
clip: true
|
||||
spacing: 0
|
||||
animateAppearance: false
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Bluetooth.devices.values].sort((a, b) => (b.connected - a.connected) || (b.paired - a.paired))
|
||||
}
|
||||
delegate: BluetoothDeviceItem {
|
||||
required property BluetoothDevice modelData
|
||||
device: modelData
|
||||
anchors {
|
||||
left: parent?.left
|
||||
right: parent?.right
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowDialogSeparator {}
|
||||
WindowDialogButtonRow {
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Details")
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]);
|
||||
GlobalStates.sidebarRightOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Done")
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
@@ -10,13 +11,10 @@ import Quickshell.Hyprland
|
||||
|
||||
QuickToggleButton {
|
||||
id: root
|
||||
readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled
|
||||
readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected)
|
||||
readonly property bool bluetoothConnected: bluetoothDevice !== undefined
|
||||
toggled: bluetoothEnabled
|
||||
buttonIcon: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
|
||||
toggled: BluetoothStatus.enabled
|
||||
buttonIcon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||
onClicked: {
|
||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled
|
||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
|
||||
}
|
||||
altAction: () => {
|
||||
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`])
|
||||
@@ -24,7 +22,8 @@ QuickToggleButton {
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("%1 | Right-click to configure").arg(
|
||||
(bluetoothDevice?.name.length > 0) ?
|
||||
bluetoothDevice.name : Translation.tr("Bluetooth"))
|
||||
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
|
||||
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.services.network
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
WindowDialog {
|
||||
id: root
|
||||
|
||||
WindowDialogTitle {
|
||||
text: Translation.tr("Connect to Wi-Fi")
|
||||
}
|
||||
WindowDialogSeparator {
|
||||
visible: !Network.wifiScanning
|
||||
}
|
||||
StyledIndeterminateProgressBar {
|
||||
visible: Network.wifiScanning
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -8
|
||||
Layout.bottomMargin: -8
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
}
|
||||
ListView {
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: -15
|
||||
Layout.bottomMargin: -16
|
||||
Layout.leftMargin: -Appearance.rounding.large
|
||||
Layout.rightMargin: -Appearance.rounding.large
|
||||
|
||||
clip: true
|
||||
spacing: 0
|
||||
|
||||
model: ScriptModel {
|
||||
values: [...Network.wifiNetworks].sort((a, b) => {
|
||||
if (a.active && !b.active)
|
||||
return -1;
|
||||
if (!a.active && b.active)
|
||||
return 1;
|
||||
return b.strength - a.strength;
|
||||
})
|
||||
}
|
||||
delegate: WifiNetworkItem {
|
||||
required property WifiAccessPoint modelData
|
||||
wifiNetwork: modelData
|
||||
anchors {
|
||||
left: parent?.left
|
||||
right: parent?.right
|
||||
}
|
||||
}
|
||||
}
|
||||
WindowDialogSeparator {}
|
||||
WindowDialogButtonRow {
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Details")
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]);
|
||||
GlobalStates.sidebarRightOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Done")
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.services.network
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
DialogListItem {
|
||||
id: root
|
||||
required property WifiAccessPoint wifiNetwork
|
||||
|
||||
active: (wifiNetwork?.askingPassword || wifiNetwork?.active) ?? false
|
||||
onClicked: {
|
||||
Network.connectToWifiNetwork(wifiNetwork);
|
||||
}
|
||||
|
||||
contentItem: ColumnLayout {
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: root.verticalPadding
|
||||
bottomMargin: root.verticalPadding
|
||||
leftMargin: root.horizontalPadding
|
||||
rightMargin: root.horizontalPadding
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
RowLayout {
|
||||
// Name
|
||||
spacing: 10
|
||||
MaterialSymbol {
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
property int strength: root.wifiNetwork?.strength ?? 0
|
||||
text: strength > 80 ? "signal_wifi_4_bar" : strength > 60 ? "network_wifi_3_bar" : strength > 40 ? "network_wifi_2_bar" : strength > 20 ? "network_wifi_1_bar" : "signal_wifi_0_bar"
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
elide: Text.ElideRight
|
||||
text: root.wifiNetwork?.ssid ?? Translation.tr("Unknown")
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: (root.wifiNetwork?.isSecure || root.wifiNetwork?.active) ?? false
|
||||
text: root.wifiNetwork?.active ? "check" : Network.wifiConnectTarget === root.wifiNetwork ? "settings_ethernet" : "lock"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout { // Password
|
||||
id: passwordPrompt
|
||||
Layout.topMargin: 8
|
||||
visible: root.wifiNetwork?.askingPassword ?? false
|
||||
|
||||
MaterialTextField {
|
||||
id: passwordField
|
||||
Layout.fillWidth: true
|
||||
placeholderText: Translation.tr("Password")
|
||||
|
||||
// Password
|
||||
echoMode: TextInput.Password
|
||||
inputMethodHints: Qt.ImhSensitiveData
|
||||
|
||||
onAccepted: {
|
||||
Network.changePassword(root.wifiNetwork, passwordField.text);
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Cancel")
|
||||
onClicked: {
|
||||
root.wifiNetwork.askingPassword = false;
|
||||
}
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Connect")
|
||||
onClicked: {
|
||||
Network.changePassword(root.wifiNetwork, passwordField.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout { // Public wifi login page
|
||||
id: publicWifiPortal
|
||||
Layout.topMargin: 8
|
||||
visible: (root.wifiNetwork?.active && (root.wifiNetwork?.security ?? "").trim().length === 0) ?? false
|
||||
|
||||
RowLayout {
|
||||
DialogButton {
|
||||
Layout.fillWidth: true
|
||||
buttonText: Translation.tr("Open network portal")
|
||||
colBackground: Appearance.colors.colLayer4
|
||||
colBackgroundHover: Appearance.colors.colLayer4Hover
|
||||
colRipple: Appearance.colors.colLayer4Active
|
||||
onClicked: {
|
||||
Network.openPublicWifiPortal()
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillHeight: true
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,10 +285,7 @@ Item { // Bar content region
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
MaterialSymbol {
|
||||
readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled
|
||||
readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected)
|
||||
readonly property bool bluetoothConnected: bluetoothDevice !== undefined
|
||||
text: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
|
||||
text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: rightSidebarButton.colText
|
||||
}
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
required property var fileModelData
|
||||
property bool isDirectory: fileModelData.fileIsDir
|
||||
property bool useThumbnail: Images.isValidImageByName(fileModelData.fileName)
|
||||
|
||||
property alias colBackground: background.color
|
||||
property alias colText: wallpaperItemName.color
|
||||
property alias radius: background.radius
|
||||
property alias margins: background.anchors.margins
|
||||
property alias padding: wallpaperItemColumnLayout.anchors.margins
|
||||
margins: Appearance.sizes.wallpaperSelectorItemMargins
|
||||
padding: Appearance.sizes.wallpaperSelectorItemPadding
|
||||
|
||||
signal activated()
|
||||
|
||||
hoverEnabled: true
|
||||
onClicked: root.activated()
|
||||
|
||||
Rectangle {
|
||||
id: background
|
||||
anchors.fill: parent
|
||||
radius: Appearance.rounding.normal
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: wallpaperItemColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Item {
|
||||
id: wallpaperItemImageContainer
|
||||
Layout.fillHeight: true
|
||||
Layout.fillWidth: true
|
||||
|
||||
Loader {
|
||||
id: thumbnailShadowLoader
|
||||
active: thumbnailImageLoader.active && thumbnailImageLoader.item.status === Image.Ready
|
||||
anchors.fill: thumbnailImageLoader
|
||||
sourceComponent: StyledRectangularShadow {
|
||||
target: thumbnailImageLoader
|
||||
anchors.fill: undefined
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: thumbnailImageLoader
|
||||
anchors.fill: parent
|
||||
active: root.useThumbnail
|
||||
sourceComponent: ThumbnailImage {
|
||||
id: thumbnailImage
|
||||
generateThumbnail: false
|
||||
sourcePath: fileModelData.filePath
|
||||
|
||||
fillMode: Image.PreserveAspectCrop
|
||||
clip: true
|
||||
sourceSize.width: wallpaperItemColumnLayout.width
|
||||
sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height
|
||||
|
||||
Connections {
|
||||
target: Wallpapers
|
||||
function onThumbnailGenerated(directory) {
|
||||
if (thumbnailImage.status !== Image.Error) return;
|
||||
if (FileUtils.parentDirectory(thumbnailImage.sourcePath) !== directory) return;
|
||||
thumbnailImage.source = "";
|
||||
thumbnailImage.source = thumbnailImage.thumbnailPath;
|
||||
}
|
||||
function onThumbnailGeneratedFile(filePath) {
|
||||
if (thumbnailImage.status !== Image.Error) return;
|
||||
if (Qt.resolvedUrl(thumbnailImage.sourcePath) !== Qt.resolvedUrl(filePath)) return;
|
||||
thumbnailImage.source = "";
|
||||
thumbnailImage.source = thumbnailImage.thumbnailPath;
|
||||
}
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: wallpaperItemImageContainer.width
|
||||
height: wallpaperItemImageContainer.height
|
||||
radius: Appearance.rounding.small
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: iconLoader
|
||||
active: !root.useThumbnail
|
||||
anchors.fill: parent
|
||||
sourceComponent: DirectoryIcon {
|
||||
fileModelData: root.fileModelData
|
||||
sourceSize.width: wallpaperItemColumnLayout.width
|
||||
sourceSize.height: wallpaperItemColumnLayout.height - wallpaperItemColumnLayout.spacing - wallpaperItemName.height
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: wallpaperItemName
|
||||
Layout.fillWidth: true
|
||||
Layout.leftMargin: 10
|
||||
Layout.rightMargin: 10
|
||||
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
elide: Text.ElideRight
|
||||
font.pixelSize: Appearance.font.pixelSize.smaller
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
text: fileModelData.fileName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
Loader {
|
||||
id: wallpaperSelectorLoader
|
||||
active: GlobalStates.wallpaperSelectorOpen
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: panelWindow
|
||||
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen)
|
||||
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
|
||||
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
WlrLayershell.namespace: "quickshell:wallpaperSelector"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
color: "transparent"
|
||||
|
||||
anchors.top: true
|
||||
margins {
|
||||
top: Config?.options.bar.vertical ? Appearance.sizes.hyprlandGapsOut : Appearance.sizes.barHeight + Appearance.sizes.hyprlandGapsOut
|
||||
}
|
||||
|
||||
mask: Region {
|
||||
item: content
|
||||
}
|
||||
|
||||
implicitHeight: Appearance.sizes.wallpaperSelectorHeight
|
||||
implicitWidth: Appearance.sizes.wallpaperSelectorWidth
|
||||
|
||||
HyprlandFocusGrab { // Click outside to close
|
||||
id: grab
|
||||
windows: [ panelWindow ]
|
||||
active: wallpaperSelectorLoader.active
|
||||
onCleared: () => {
|
||||
if (!active) GlobalStates.wallpaperSelectorOpen = false;
|
||||
}
|
||||
}
|
||||
|
||||
WallpaperSelectorContent {
|
||||
id: content
|
||||
anchors {
|
||||
fill: parent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "wallpaperSelector"
|
||||
|
||||
function toggle(): void {
|
||||
GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "wallpaperSelectorToggle"
|
||||
description: "Toggle wallpaper selector"
|
||||
onPressed: {
|
||||
GlobalStates.wallpaperSelectorOpen = !GlobalStates.wallpaperSelectorOpen;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,389 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int columns: 4
|
||||
property real previewCellAspectRatio: 4 / 3
|
||||
property bool useDarkMode: Appearance.m3colors.darkmode
|
||||
|
||||
function updateThumbnails() {
|
||||
const totalImageMargin = (Appearance.sizes.wallpaperSelectorItemMargins + Appearance.sizes.wallpaperSelectorItemPadding) * 2
|
||||
const thumbnailSizeName = Images.thumbnailSizeNameForDimensions(grid.cellWidth - totalImageMargin, grid.cellHeight - totalImageMargin)
|
||||
Wallpapers.generateThumbnail(thumbnailSizeName)
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Wallpapers
|
||||
function onDirectoryChanged() {
|
||||
root.updateThumbnails()
|
||||
}
|
||||
}
|
||||
|
||||
function handleFilePasting(event) {
|
||||
const currentClipboardEntry = Cliphist.entries[0]
|
||||
if (/^\d+\tfile:\/\/\S+/.test(currentClipboardEntry)) {
|
||||
const url = StringUtils.cleanCliphistEntry(currentClipboardEntry);
|
||||
Wallpapers.setDirectory(FileUtils.trimFileProtocol(decodeURIComponent(url)));
|
||||
event.accepted = true;
|
||||
} else {
|
||||
event.accepted = false; // No image, let text pasting proceed
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
GlobalStates.wallpaperSelectorOpen = false;
|
||||
event.accepted = true;
|
||||
} else if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) { // Intercept Ctrl+V to handle "paste to go to" in pickers
|
||||
root.handleFilePasting(event);
|
||||
} else if (event.modifiers & Qt.AltModifier && event.key === Qt.Key_Up) {
|
||||
Wallpapers.setDirectory(FileUtils.parentDirectory(Wallpapers.directory));
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Left) {
|
||||
grid.moveSelection(-1);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Right) {
|
||||
grid.moveSelection(1);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Up) {
|
||||
grid.moveSelection(-grid.columns);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
grid.moveSelection(grid.columns);
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
|
||||
grid.activateCurrent();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Backspace) {
|
||||
if (filterField.text.length > 0) {
|
||||
filterField.text = filterField.text.substring(0, filterField.text.length - 1);
|
||||
}
|
||||
filterField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
} else if (event.modifiers & Qt.ControlModifier && event.key === Qt.Key_L) {
|
||||
addressBar.focusBreadcrumb();
|
||||
event.accepted = true;
|
||||
} else if (event.key === Qt.Key_Slash) {
|
||||
filterField.forceActiveFocus();
|
||||
event.accepted = true;
|
||||
} else {
|
||||
if (event.text.length > 0) {
|
||||
filterField.text += event.text;
|
||||
filterField.cursorPosition = filterField.text.length;
|
||||
filterField.forceActiveFocus();
|
||||
}
|
||||
event.accepted = true;
|
||||
}
|
||||
}
|
||||
|
||||
implicitHeight: mainLayout.implicitHeight
|
||||
implicitWidth: mainLayout.implicitWidth
|
||||
|
||||
StyledRectangularShadow {
|
||||
target: wallpaperGridBackground
|
||||
}
|
||||
Rectangle {
|
||||
id: wallpaperGridBackground
|
||||
anchors {
|
||||
fill: parent
|
||||
margins: Appearance.sizes.elevationMargin
|
||||
}
|
||||
focus: true
|
||||
border.width: 1
|
||||
border.color: Appearance.colors.colLayer0Border
|
||||
color: Appearance.colors.colLayer0
|
||||
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
|
||||
|
||||
property int calculatedRows: Math.ceil(grid.count / grid.columns)
|
||||
|
||||
implicitWidth: gridColumnLayout.implicitWidth
|
||||
implicitHeight: gridColumnLayout.implicitHeight
|
||||
|
||||
RowLayout {
|
||||
id: mainLayout
|
||||
anchors.fill: parent
|
||||
spacing: -4
|
||||
|
||||
Rectangle {
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: 4
|
||||
implicitWidth: quickDirColumnLayout.implicitWidth
|
||||
implicitHeight: quickDirColumnLayout.implicitHeight
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: wallpaperGridBackground.radius - Layout.margins
|
||||
|
||||
ColumnLayout {
|
||||
id: quickDirColumnLayout
|
||||
anchors.fill: parent
|
||||
spacing: 0
|
||||
|
||||
StyledText {
|
||||
Layout.margins: 12
|
||||
font {
|
||||
pixelSize: Appearance.font.pixelSize.normal
|
||||
weight: Font.Medium
|
||||
}
|
||||
text: Translation.tr("Pick a wallpaper")
|
||||
}
|
||||
ListView {
|
||||
// Quick dirs
|
||||
Layout.fillHeight: true
|
||||
Layout.margins: 4
|
||||
implicitWidth: 140
|
||||
clip: true
|
||||
model: [
|
||||
{ icon: "home", name: "Home", path: Directories.home },
|
||||
{ icon: "docs", name: "Documents", path: Directories.documents },
|
||||
{ icon: "download", name: "Downloads", path: Directories.downloads },
|
||||
{ icon: "image", name: "Pictures", path: Directories.pictures },
|
||||
{ icon: "movie", name: "Videos", path: Directories.videos },
|
||||
{ icon: "", name: "---", path: "INTENTIONALLY_INVALID_DIR" },
|
||||
{ icon: "wallpaper", name: "Wallpapers", path: `${Directories.pictures}/Wallpapers` },
|
||||
{ icon: "favorite", name: "Homework", path: `${Directories.pictures}/homework` },
|
||||
]
|
||||
delegate: RippleButton {
|
||||
id: quickDirButton
|
||||
required property var modelData
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
onClicked: Wallpapers.setDirectory(quickDirButton.modelData.path)
|
||||
enabled: modelData.icon.length > 0
|
||||
toggled: Wallpapers.directory === FileUtils.trimFileProtocol(modelData.path)
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: RowLayout {
|
||||
MaterialSymbol {
|
||||
color: quickDirButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: quickDirButton.modelData.icon
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
horizontalAlignment: Text.AlignLeft
|
||||
color: quickDirButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1
|
||||
text: quickDirButton.modelData.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: gridColumnLayout
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
AddressBar {
|
||||
id: addressBar
|
||||
Layout.margins: 4
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: false
|
||||
directory: Wallpapers.directory
|
||||
onNavigateToDirectory: path => {
|
||||
Wallpapers.setDirectory(path.length == 0 ? "/" : path);
|
||||
}
|
||||
radius: wallpaperGridBackground.radius - Layout.margins
|
||||
}
|
||||
|
||||
Item {
|
||||
id: gridDisplayRegion
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
StyledIndeterminateProgressBar {
|
||||
id: indeterminateProgressBar
|
||||
visible: Wallpapers.thumbnailGenerationRunning && value == 0
|
||||
anchors {
|
||||
bottom: parent.top
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: 4
|
||||
rightMargin: 4
|
||||
}
|
||||
}
|
||||
|
||||
StyledProgressBar {
|
||||
visible: Wallpapers.thumbnailGenerationRunning && value > 0
|
||||
value: Wallpapers.thumbnailGenerationProgress
|
||||
anchors.fill: indeterminateProgressBar
|
||||
}
|
||||
|
||||
GridView {
|
||||
id: grid
|
||||
visible: Wallpapers.folderModel.count > 0
|
||||
|
||||
readonly property int columns: root.columns
|
||||
readonly property int rows: Math.max(1, Math.ceil(count / columns))
|
||||
property int currentIndex: 0
|
||||
|
||||
anchors.fill: parent
|
||||
cellWidth: width / root.columns
|
||||
cellHeight: cellWidth / root.previewCellAspectRatio
|
||||
interactive: true
|
||||
clip: true
|
||||
keyNavigationWraps: true
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
bottomMargin: extraOptions.implicitHeight
|
||||
ScrollBar.vertical: StyledScrollBar {}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.updateThumbnails()
|
||||
}
|
||||
|
||||
function moveSelection(delta) {
|
||||
currentIndex = Math.max(0, Math.min(grid.model.count - 1, currentIndex + delta));
|
||||
positionViewAtIndex(currentIndex, GridView.Contain);
|
||||
}
|
||||
|
||||
function activateCurrent() {
|
||||
const filePath = grid.model.get(currentIndex, "filePath")
|
||||
Wallpapers.select(filePath, root.useDarkMode);
|
||||
filterField.text = "";
|
||||
}
|
||||
|
||||
model: Wallpapers.folderModel
|
||||
onModelChanged: currentIndex = 0
|
||||
delegate: WallpaperDirectoryItem {
|
||||
required property var modelData
|
||||
required property int index
|
||||
fileModelData: modelData
|
||||
width: grid.cellWidth
|
||||
height: grid.cellHeight
|
||||
colBackground: (index === grid?.currentIndex || containsMouse) ? Appearance.colors.colPrimary : (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colSecondaryContainer : ColorUtils.transparentize(Appearance.colors.colPrimaryContainer)
|
||||
colText: (index === grid.currentIndex || containsMouse) ? Appearance.colors.colOnPrimary : (fileModelData.filePath === Config.options.background.wallpaperPath) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer0
|
||||
|
||||
onEntered: {
|
||||
grid.currentIndex = index;
|
||||
}
|
||||
|
||||
onActivated: {
|
||||
Wallpapers.select(fileModelData.filePath, root.useDarkMode);
|
||||
filterField.text = "";
|
||||
}
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: gridDisplayRegion.width
|
||||
height: gridDisplayRegion.height
|
||||
radius: wallpaperGridBackground.radius
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Toolbar {
|
||||
id: extraOptions
|
||||
anchors {
|
||||
bottom: parent.bottom
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottomMargin: 8
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
implicitWidth: height
|
||||
onClicked: {
|
||||
Wallpapers.openFallbackPicker(root.useDarkMode);
|
||||
GlobalStates.wallpaperSelectorOpen = false;
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
text: "open_in_new"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Use the system file picker instead")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
implicitWidth: height
|
||||
onClicked: root.useDarkMode = !root.useDarkMode
|
||||
contentItem: MaterialSymbol {
|
||||
text: root.useDarkMode ? "dark_mode" : "light_mode"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
StyledToolTip {
|
||||
content: Translation.tr("Click to toggle light/dark mode (applied when wallpaper is chosen)")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarTextField {
|
||||
id: filterField
|
||||
placeholderText: focus ? Translation.tr("Search wallpapers") : Translation.tr("Hit \"/\" to search")
|
||||
|
||||
// Style
|
||||
clip: true
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
|
||||
// Search
|
||||
onTextChanged: {
|
||||
Wallpapers.searchQuery = text;
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if ((event.modifiers & Qt.ControlModifier) && event.key === Qt.Key_V) { // Intercept Ctrl+V to handle "paste to go to" in pickers
|
||||
root.handleFilePasting(event);
|
||||
return;
|
||||
}
|
||||
else if (text.length !== 0) {
|
||||
// No filtering, just navigate grid
|
||||
if (event.key === Qt.Key_Down) {
|
||||
grid.moveSelection(grid.columns);
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
if (event.key === Qt.Key_Up) {
|
||||
grid.moveSelection(-grid.columns);
|
||||
event.accepted = true;
|
||||
return;
|
||||
}
|
||||
}
|
||||
event.accepted = false;
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
onClicked: {
|
||||
GlobalStates.wallpaperSelectorOpen = false;
|
||||
}
|
||||
contentItem: StyledText {
|
||||
text: "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onWallpaperSelectorOpenChanged() {
|
||||
if (GlobalStates.wallpaperSelectorOpen && monitorIsFocused) {
|
||||
filterField.forceActiveFocus();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Connections {
|
||||
target: Wallpapers
|
||||
function onChanged() {
|
||||
GlobalStates.wallpaperSelectorOpen = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user