Merge branch 'main' into background-clock-setting

This commit is contained in:
end-4
2025-08-31 13:05:04 +07:00
committed by GitHub
68 changed files with 2756 additions and 331 deletions
+2 -1
View File
@@ -48,7 +48,8 @@ bindld = Super+Shift,M, Toggle mute, exec, wpctl set-mute @DEFAULT_SINK@ toggle
bindl = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
bindl = ,XF86AudioMicMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
bindld = Super+Alt,M, Toggle mic, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
bindd = Ctrl+Super, T, Change wallpaper, exec, ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # Change wallpaper
bindd = Ctrl+Super, T, Toggle wallpaper selector, global, quickshell:wallpaperSelectorToggle # Wallpaper selector
bindd = Ctrl+Super, T, Change wallpaper, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/quickshell/$qsConfig/scripts/colors/switchwall.sh # [hidden] Change wallpaper (fallback)
bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets
##! Utilities
+2
View File
@@ -134,8 +134,10 @@ layerrule = animation slide, quickshell:verticalBar
layerrule = animation fade, quickshell:screenCorners
layerrule = animation slide right, quickshell:sidebarRight
layerrule = animation slide left, quickshell:sidebarLeft
layerrule = animation slide top, quickshell:wallpaperSelector
layerrule = animation slide bottom, quickshell:osk
layerrule = animation slide bottom, quickshell:dock
layerrule = animation slide bottom, quickshell:cheatsheet
layerrule = blur, quickshell:session
layerrule = noanim, quickshell:session
layerrule = ignorealpha 0, quickshell:session
+1
View File
@@ -17,6 +17,7 @@ Singleton {
property bool osdVolumeOpen: false
property bool oskOpen: false
property bool overviewOpen: false
property bool wallpaperSelectorOpen: false
property bool screenLocked: false
property bool screenLockContainsCharacters: false
property bool screenUnlockFailed: false
@@ -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, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;');
}
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;
}
}
}
@@ -0,0 +1,128 @@
#!/usr/bin/env bash
# Generate thumbnails for files using ImageMagick, following Freedesktop spec
# Usage:
# ./generate-thumbnails-magick.sh --file <path>
# ./generate-thumbnails-magick.sh --directory <path>
set -e
# Thumbnail sizes mapping
get_thumbnail_size() {
case "$1" in
normal) echo 128 ;;
large) echo 256 ;;
x-large) echo 512 ;;
xx-large) echo 1024 ;;
*) echo 128 ;;
esac
}
usage() {
echo "Usage: $0 --file <path> | --directory <path>"
exit 1
}
md5() {
# Calculate md5 hash of the file's absolute path
echo -n "$1" | md5sum | awk '{print $1}'
}
urlencode() {
# Percent-encode a string for use in a URI, but do not encode slashes
local str="$1"
local encoded=""
local c
for ((i=0; i<${#str}; i++)); do
c="${str:$i:1}"
case "$c" in
[a-zA-Z0-9.~_-]|/) encoded+="$c" ;;
*) printf -v hex '%%%02X' "'${c}'"; encoded+="$hex" ;;
esac
done
echo "$encoded"
}
generate_thumbnail() {
local src="$1"
local abs_path
abs_path="$(realpath "$src")"
# Skip files with multiple frames (GIFs, videos, etc.)
case "${abs_path,,}" in
*.gif|*.mp4|*.webm|*.mkv|*.avi|*.mov)
return
;;
esac
local encoded_path
encoded_path="$(urlencode "$abs_path")"
local uri
uri="file://$encoded_path"
local hash
hash="$(md5 "$uri")"
local out="$CACHE_DIR/$hash.png"
mkdir -p "$CACHE_DIR"
if [ -f "$out" ]; then
return
fi
magick "$abs_path" -resize "${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE}" "$out"
}
# Parse arguments
SIZE_NAME="normal"
MODE=""
TARGET=""
while [[ $# -gt 0 ]]; do
case "$1" in
--file|-f)
MODE="file"
TARGET="$2"
shift 2
;;
--directory|-d)
MODE="dir"
TARGET="$2"
shift 2
;;
--size|-s)
SIZE_NAME="$2"
shift 2
;;
*)
usage
;;
esac
# Only one mode allowed
[[ -n "$MODE" ]] && break
done
THUMBNAIL_SIZE="$(get_thumbnail_size "$SIZE_NAME")"
CACHE_DIR="$HOME/.cache/thumbnails/$SIZE_NAME"
if [ -z "$MODE" ] || [ -z "$TARGET" ]; then
usage
fi
case "$MODE" in
file)
if [ ! -f "$TARGET" ]; then
echo "File not found: $TARGET"
exit 2
fi
generate_thumbnail "$TARGET"
;;
dir)
if [ ! -d "$TARGET" ]; then
echo "Directory not found: $TARGET"
exit 2
fi
for f in "$TARGET"/*; do
[ -f "$f" ] || continue
generate_thumbnail "$f" &
done
wait
;;
*)
usage
;;
esac
+119
View File
@@ -0,0 +1,119 @@
#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$(eval\_echo\_\$ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
# From https://github.com/difference-engine/thumbnail-generator-ubuntu (MIT License)
# Since the script is small and the maintainers seem inactive to accept my PR (#11) I decided to just copy it over.
# When it gets merged and the python package gets updated we can just use it
import os
import sys
from multiprocessing import Pool
from pathlib import Path
from typing import List, Union
import click
import gi
from loguru import logger
from tqdm import tqdm
gi.require_version("GnomeDesktop", "3.0")
from gi.repository import Gio, GnomeDesktop # isort:skip
thumbnail_size_map = {
"normal": GnomeDesktop.DesktopThumbnailSize.NORMAL,
"large": GnomeDesktop.DesktopThumbnailSize.LARGE,
"x-large": GnomeDesktop.DesktopThumbnailSize.XLARGE,
"xx-large": GnomeDesktop.DesktopThumbnailSize.XXLARGE,
}
factory = None
logger.remove()
logger.add(sys.stdout, level="INFO")
logger.add("/tmp/thumbgen.log", level="DEBUG", rotation="100 MB")
def make_thumbnail(fpath: str) -> bool:
mtime = os.path.getmtime(fpath)
# Use Gio to determine the URI and mime type
f = Gio.file_new_for_path(str(fpath))
uri = f.get_uri()
info = f.query_info("standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
mime_type = info.get_content_type()
if factory.lookup(uri, mtime) is not None:
logger.debug("FRESH {}".format(uri))
return False
if not factory.can_thumbnail(uri, mime_type, mtime):
logger.debug("UNSUPPORTED {}".format(uri))
return False
thumbnail = factory.generate_thumbnail(uri, mime_type)
if thumbnail is None:
logger.debug("ERROR {}".format(uri))
return False
logger.debug("OK {}".format(uri))
factory.save_thumbnail(thumbnail, uri, mtime)
return True
@logger.catch()
def thumbnail_folder(*, dir_path: Path, workers: int, only_images: bool, recursive: bool, machine_progress: bool = False) -> None:
all_files = get_all_files(dir_path=dir_path, recursive=recursive)
if only_images:
all_files = get_all_images(all_files=all_files)
all_files = [str(fpath) for fpath in all_files]
if machine_progress:
completed = 0
total = len(all_files)
with Pool(processes=workers) as p:
for result in p.imap(make_thumbnail, all_files):
completed += 1
print(f"PROGRESS {completed}/{total} FILE {all_files[completed-1]}")
sys.stdout.flush()
else:
with Pool(processes=workers) as p:
list(tqdm(p.imap(make_thumbnail, all_files), total=len(all_files)))
def get_all_images(*, all_files: List[Path]) -> List[Path]:
img_suffixes = [".jpg", ".jpeg", ".png", ".gif"]
all_images = [fpath for fpath in all_files if fpath.suffix in img_suffixes]
print("Found {} images".format(len(all_images)))
return all_images
def get_all_files(*, dir_path: Path, recursive: bool) -> List[Path]:
if not (dir_path.exists() and dir_path.is_dir()):
raise ValueError("{} doesn't exist or isn't a valid directory!".format(dir_path.resolve()))
if recursive:
all_files = dir_path.rglob("*")
else:
all_files = dir_path.glob("*")
all_files = [fpath for fpath in all_files if fpath.is_file()]
print("Found {} files in the directory: {}".format(len(all_files), dir_path.resolve()))
return all_files
@click.command()
@click.option(
"-d", "--img_dirs", required=True, help='directories to generate thumbnails seperated by space, eg: "dir1/dir2 dir3"'
)
@click.option(
"-s", "--size", default="normal", type=click.Choice(["normal", "large", "x-large", "xx-large"]), help="Thumbnail size: normal, large, x-large, xx-large"
)
@click.option("-w", "--workers", default=1, help="no of cpus to use for processing")
@click.option(
"-i", "--only_images", is_flag=True, default=False, help="Whether to only look for images to be thumbnailed"
)
@click.option("-r", "--recursive", is_flag=True, default=False, help="Whether to recursively look for files")
@click.option("--machine_progress", is_flag=True, default=False, help="Print machine-readable progress lines instead of a progress bar")
def main(img_dirs: str, size: str, workers: str, only_images: bool, recursive: bool, machine_progress: bool) -> None:
img_dirs = [Path(img_dir) for img_dir in img_dirs.split()]
global factory
factory = GnomeDesktop.DesktopThumbnailFactory.new(thumbnail_size_map[size])
for img_dir in img_dirs:
thumbnail_folder(dir_path=img_dir, workers=workers, only_images=only_images, recursive=recursive, machine_progress=machine_progress)
print("Thumbnail Generation Completed!")
if __name__ == "__main__":
main()
@@ -0,0 +1,19 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Bluetooth
import Quickshell.Io
import QtQuick
/**
* Network service with nmcli.
*/
Singleton {
id: root
readonly property bool enabled: Bluetooth.defaultAdapter?.enabled ?? false
readonly property BluetoothDevice firstActiveDevice: Bluetooth.defaultAdapter?.devices.values.find(device => device.connected) ?? null
readonly property int activeDeviceCount: Bluetooth.defaultAdapter?.devices.values.filter(device => device.connected).length ?? 0
readonly property bool connected: Bluetooth.devices.values.some(d => d.connected)
}
+1 -1
View File
@@ -259,7 +259,7 @@ Singleton {
// Alcy doesn't provide dimensions and images are often of god resolution
"width": 1000,
"height": 1000,
"aspect_ratio": 1, // Default aspect ratio
"aspect_ratio": 1,
"tags": "[no tags]",
"rating": "s",
"is_nsfw": false,
+2 -1
View File
@@ -1,3 +1,4 @@
import qs
import qs.modules.common
import QtQuick
import Quickshell
@@ -11,7 +12,7 @@ pragma ComponentBehavior: Bound
Singleton {
property var clock: SystemClock {
id: clock
precision: SystemClock.Minutes
precision: GlobalStates.screenLocked ? SystemClock.Seconds : SystemClock.Minutes // Hack to ensure clock is correct after waking up from suspend
}
property string time: Qt.locale().toString(clock.date, Config.options?.time.format ?? "hh:mm")
property string shortDate: Qt.locale().toString(clock.date, Config.options?.time.shortDateFormat ?? "dd/MM")
@@ -13,10 +13,10 @@ import Quickshell.Io
Singleton {
id: root
property var manualActive
property string from: Config.options?.light?.night?.from ?? "19:00" // Default to 7 PM
property string to: Config.options?.light?.night?.to ?? "06:30" // Default to 6:30 AM
property string from: Config.options?.light?.night?.from ?? "19:00"
property string to: Config.options?.light?.night?.to ?? "06:30"
property bool automatic: Config.options?.light?.night?.automatic && (Config?.ready ?? true)
property int colorTemperature: Config.options?.light?.night?.colorTemperature ?? 5000 // Default color temperature
property int colorTemperature: Config.options?.light?.night?.colorTemperature ?? 5000
property bool shouldBeOn
property bool firstEvaluation: true
property bool active: false
+174 -3
View File
@@ -1,12 +1,15 @@
pragma Singleton
pragma ComponentBehavior: Bound
// Took many bits from https://github.com/caelestia-dots/shell (GPLv3)
import Quickshell
import Quickshell.Io
import QtQuick
import "./network"
/**
* Simple polled network state service.
* Network service with nmcli.
*/
Singleton {
id: root
@@ -15,6 +18,12 @@ Singleton {
property bool ethernet: false
property bool wifiEnabled: false
property bool wifiScanning: false
property bool wifiConnecting: connectProc.running
property WifiAccessPoint wifiConnectTarget
readonly property list<WifiAccessPoint> wifiNetworks: []
readonly property WifiAccessPoint active: wifiNetworks.find(n => n.active) ?? null
property string networkName: ""
property int networkStrength
property string materialSymbol: ethernet ? "lan" :
@@ -27,15 +36,103 @@ Singleton {
) : "signal_wifi_off"
// Control
function toggleWifi(): void {
const cmd = wifiEnabled ? "off" : "on";
function enableWifi(enabled = true): void {
const cmd = enabled ? "on" : "off";
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]);
}
function toggleWifi(): void {
enableWifi(!wifiEnabled);
}
function rescanWifi(): void {
wifiScanning = true;
rescanProcess.running = true;
}
function connectToWifiNetwork(accessPoint: WifiAccessPoint): void {
accessPoint.askingPassword = false;
root.wifiConnectTarget = accessPoint;
// We use this instead of `nmcli connection up SSID` because this also creates a connection profile
connectProc.exec(["nmcli", "dev", "wifi", "connect", accessPoint.ssid])
}
function disconnectWifiNetwork(): void {
if (active) disconnectProc.exec(["nmcli", "connection", "down", active.ssid]);
}
function openPublicWifiPortal() {
Quickshell.execDetached(["xdg-open", "https://nmcheck.gnome.org/"]) // From some StackExchange thread, seems to work
}
function changePassword(network: WifiAccessPoint, password: string, username = ""): void {
// TODO: enterprise wifi with username
network.askingPassword = false;
changePasswordProc.exec({
"environment": {
"PASSWORD": password
},
"command": ["bash", "-c", `nmcli connection modify ${network.ssid} wifi-sec.psk "$PASSWORD"`]
})
}
Process {
id: enableWifiProc
}
Process {
id: connectProc
environment: ({
LANG: "C",
LC_ALL: "C"
})
stdout: SplitParser {
onRead: line => {
// print(line)
getNetworks.running = true
}
}
stderr: SplitParser {
onRead: line => {
// print("err:", line)
if (line.includes("Secrets were required")) {
root.wifiConnectTarget.askingPassword = true
}
}
}
onExited: (exitCode, exitStatus) => {
root.wifiConnectTarget.askingPassword = (exitCode !== 0)
root.wifiConnectTarget = null
}
}
Process {
id: disconnectProc
stdout: SplitParser {
onRead: getNetworks.running = true
}
}
Process {
id: changePasswordProc
onExited: { // Re-attempt connection after changing password
connectProc.running = false
connectProc.running = true
}
}
Process {
id: rescanProcess
command: ["nmcli", "dev", "wifi", "list", "--rescan", "yes"]
stdout: SplitParser {
onRead: {
wifiScanning = false;
getNetworks.running = true;
}
}
}
// Status update
function update() {
updateConnectionType.startCheck();
@@ -118,4 +215,78 @@ Singleton {
}
}
}
Process {
id: getNetworks
running: true
command: ["nmcli", "-g", "ACTIVE,SIGNAL,FREQ,SSID,BSSID,SECURITY", "d", "w"]
environment: ({
LANG: "C",
LC_ALL: "C"
})
stdout: StdioCollector {
onStreamFinished: {
const PLACEHOLDER = "STRINGWHICHHOPEFULLYWONTBEUSED";
const rep = new RegExp("\\\\:", "g");
const rep2 = new RegExp(PLACEHOLDER, "g");
const allNetworks = text.trim().split("\n").map(n => {
const net = n.replace(rep, PLACEHOLDER).split(":");
return {
active: net[0] === "yes",
strength: parseInt(net[1]),
frequency: parseInt(net[2]),
ssid: net[3],
bssid: net[4]?.replace(rep2, ":") ?? "",
security: net[5] || ""
};
}).filter(n => n.ssid && n.ssid.length > 0);
// Group networks by SSID and prioritize connected ones
const networkMap = new Map();
for (const network of allNetworks) {
const existing = networkMap.get(network.ssid);
if (!existing) {
networkMap.set(network.ssid, network);
} else {
// Prioritize active/connected networks
if (network.active && !existing.active) {
networkMap.set(network.ssid, network);
} else if (!network.active && !existing.active) {
// If both are inactive, keep the one with better signal
if (network.strength > existing.strength) {
networkMap.set(network.ssid, network);
}
}
// If existing is active and new is not, keep existing
}
}
const wifiNetworks = Array.from(networkMap.values());
const rNetworks = root.wifiNetworks;
const destroyed = rNetworks.filter(rn => !wifiNetworks.find(n => n.frequency === rn.frequency && n.ssid === rn.ssid && n.bssid === rn.bssid));
for (const network of destroyed)
rNetworks.splice(rNetworks.indexOf(network), 1).forEach(n => n.destroy());
for (const network of wifiNetworks) {
const match = rNetworks.find(n => n.frequency === network.frequency && n.ssid === network.ssid && n.bssid === network.bssid);
if (match) {
match.lastIpcObject = network;
} else {
rNetworks.push(apComp.createObject(root, {
lastIpcObject: network
}));
}
}
}
}
}
Component {
id: apComp
WifiAccessPoint {}
}
}
@@ -0,0 +1,160 @@
import qs.modules.common
import qs.modules.common.functions
import QtQuick
import Qt.labs.folderlistmodel
import Quickshell
import Quickshell.Io
pragma Singleton
pragma ComponentBehavior: Bound
/**
* Provides a list of wallpapers and an "apply" action that calls the existing
* switchwall.sh script. Pretty much a limited file browsing service.
*/
Singleton {
id: root
property string thumbgenScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/thumbgen.py`
property string generateThumbnailsMagicScriptPath: `${FileUtils.trimFileProtocol(Directories.scriptPath)}/thumbnails/generate-thumbnails-magick.sh`
property string directory: FileUtils.trimFileProtocol(`${Directories.pictures}/Wallpapers`)
property alias folderModel: folderModel // Expose for direct binding when needed
property string searchQuery: ""
readonly property list<string> extensions: [ // TODO: add videos
"jpg", "jpeg", "png", "webp", "avif", "bmp", "svg"
]
property list<string> wallpapers: [] // List of absolute file paths (without file://)
readonly property bool thumbnailGenerationRunning: thumbgenProc.running
property real thumbnailGenerationProgress: 0
signal changed()
signal thumbnailGenerated(directory: string)
signal thumbnailGeneratedFile(filePath: string)
// Executions
Process {
id: applyProc
}
function openFallbackPicker(darkMode = Appearance.m3colors.darkmode) {
applyProc.exec([
Directories.wallpaperSwitchScriptPath,
"--mode", (darkMode ? "dark" : "light")
])
}
function apply(path, darkMode = Appearance.m3colors.darkmode) {
if (!path || path.length === 0) return
applyProc.exec([
Directories.wallpaperSwitchScriptPath,
"--image", path,
"--mode", (darkMode ? "dark" : "light")
])
root.changed()
}
Process {
id: selectProc
property string filePath: ""
property bool darkMode: Appearance.m3colors.darkmode
function select(filePath, darkMode = Appearance.m3colors.darkmode) {
selectProc.filePath = filePath
selectProc.darkMode = darkMode
selectProc.exec(["test", "-d", FileUtils.trimFileProtocol(filePath)])
}
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
setDirectory(selectProc.filePath);
return;
}
root.apply(selectProc.filePath, selectProc.darkMode);
}
}
function select(filePath, darkMode = Appearance.m3colors.darkmode) {
selectProc.select(filePath, darkMode);
}
Process {
id: validateDirProc
property string nicePath: ""
function setDirectoryIfValid(path) {
validateDirProc.nicePath = FileUtils.trimFileProtocol(path).replace(/\/+$/, "")
if (/^\/*$/.test(validateDirProc.nicePath)) validateDirProc.nicePath = "/";
validateDirProc.exec([
"bash", "-c",
`if [ -d "${validateDirProc.nicePath}" ]; then echo dir; elif [ -f "${validateDirProc.nicePath}" ]; then echo file; else echo invalid; fi`
])
}
stdout: StdioCollector {
onStreamFinished: {
const result = text.trim()
if (result === "dir") {
root.directory = validateDirProc.nicePath
} else if (result === "file") {
root.directory = FileUtils.parentDirectory(validateDirProc.nicePath)
} else {
// Ignore
}
}
}
}
function setDirectory(path) {
validateDirProc.setDirectoryIfValid(path)
}
// Folder model
FolderListModel {
id: folderModel
folder: Qt.resolvedUrl(root.directory)
caseSensitive: false
nameFilters: root.extensions.map(ext => `*${searchQuery.split(" ").filter(s => s.length > 0).map(s => `*${s}*`)}*.${ext}`)
showDirs: true
showDotAndDotDot: false
showOnlyReadable: true
sortField: FolderListModel.Time
sortReversed: false
onCountChanged: {
root.wallpapers = []
for (let i = 0; i < folderModel.count; i++) {
const path = folderModel.get(i, "filePath") || FileUtils.trimFileProtocol(folderModel.get(i, "fileURL"))
if (path && path.length) root.wallpapers.push(path)
}
}
}
// Thumbnail generation
function generateThumbnail(size: string) {
if (!["normal", "large", "x-large", "xx-large"].includes(size)) throw new Error("Invalid thumbnail size");
thumbgenProc.directory = root.directory
thumbgenProc.running = false
thumbgenProc.command = [
"bash", "-c",
`${thumbgenScriptPath} --size ${size} --machine_progress -d ${root.directory} || ${generateThumbnailsMagicScriptPath} --size ${size} -d ${root.directory}`,
]
root.thumbnailGenerationProgress = 0
thumbgenProc.running = true
}
Process {
id: thumbgenProc
property string directory
stdout: SplitParser {
onRead: data => {
// print("thumb gen proc:", data)
let match = data.match(/PROGRESS (\d+)\/(\d+)/)
if (match) {
const completed = parseInt(match[1])
const total = parseInt(match[2])
root.thumbnailGenerationProgress = completed / total
}
match = data.match(/FILE (.+)/)
if (match) {
const filePath = match[1]
root.thumbnailGeneratedFile(filePath)
}
}
}
onExited: (exitCode, exitStatus) => {
root.thumbnailGenerated(thumbgenProc.directory)
}
}
}
@@ -0,0 +1,14 @@
import QtQuick
QtObject {
required property var lastIpcObject
readonly property string ssid: lastIpcObject.ssid
readonly property string bssid: lastIpcObject.bssid
readonly property int strength: lastIpcObject.strength
readonly property int frequency: lastIpcObject.frequency
readonly property bool active: lastIpcObject.active
readonly property string security: lastIpcObject.security
readonly property bool isSecure: security.length > 0
property bool askingPassword: false
}
+3
View File
@@ -23,6 +23,7 @@ import "./modules/sessionScreen/"
import "./modules/sidebarLeft/"
import "./modules/sidebarRight/"
import "./modules/verticalBar/"
import "./modules/wallpaperSelector/"
import QtQuick
import QtQuick.Window
@@ -49,6 +50,7 @@ ShellRoot {
property bool enableSidebarLeft: true
property bool enableSidebarRight: true
property bool enableVerticalBar: true
property bool enableWallpaperSelector: true
// Force initialization of some singletons
Component.onCompleted: {
@@ -76,5 +78,6 @@ ShellRoot {
LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} }
LazyLoader { active: enableSidebarRight; component: SidebarRight {} }
LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} }
LazyLoader { active: enableWallpaperSelector; component: WallpaperSelector {} }
}
+156 -62
View File
@@ -14,20 +14,19 @@
"Action": "操作",
"Add": "追加",
"Add task": "タスクを追加",
"All-rounder | Good quality, decent quantity": "万能型 | 品質良好、量も十分",
"All-rounder | Good quality, decent quantity": "万能型 | 品質・十分な量",
"Allow NSFW": "NSFWを許可",
"Allow NSFW content": "NSFWコンテンツを許可",
"Anime": "アニメ",
"Anime boorus": "アニメ画像掲示板",
"App": "アプリ",
"Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "矢印キーで移動、Enterで選択\nEscまたはどこかをクリックでキャンセル",
"Arrow keys to navigate, Enter to select\nEsc or click anywhere to cancel": "矢印キーで移動、Enterで選択\nEsc/クリックでキャンセル",
"Bluetooth": "Bluetooth",
"Brightness": "明るさ",
"Cancel": "キャンセル",
"Chain of Thought": "思考プロセス",
"Cheat sheet": "チートシート",
"Choose model": "モデルを選択",
"Clean stuff | Excellent quality, no NSFW": "健全 | 高品質NSFWなし",
"Clean stuff | Excellent quality, no NSFW": "健全 | 高品質NSFWなし",
"Clear": "クリア",
"Clear chat history": "チャット履歴を消去",
"Clear the current list of images": "現在の画像リストをクリア",
@@ -42,7 +41,7 @@
"Edit": "編集",
"Enter text to translate...": "翻訳するテキストを入力...",
"Finished tasks will go here": "完了したタスクはここに表示されます",
"For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 質",
"For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 高品質",
"For storing API keys and other sensitive information": "APIキーや機密情報の保存用",
"Game mode": "ゲームモード",
"Get the next page of results": "次のページを取得",
@@ -53,14 +52,13 @@
"Invalid arguments. Must provide `key` and `value`.": "無効な引数です。`key`と`value`を指定してください。",
"Jump to current month": "現在の月へ移動",
"Keep system awake": "システムをスリープさせない",
"Large images | God tier quality, no NSFW.": "大きな画像 | 最高品質NSFWなし",
"Large images | God tier quality, no NSFW.": "大きな画像 | 最高品質NSFWなし",
"Large language models": "大規模言語モデル",
"Launch": "起動",
"Lock": "ロック",
"Logout": "ログアウト",
"Markdown test": "Markdownテスト",
"Math result": "計算結果",
"Night Light": "夜間モード",
"No audio source": "音声ソースなし",
"No media": "メディアなし",
"No notifications": "通知なし",
@@ -83,7 +81,7 @@
"Select Language": "言語を選択",
"Session": "セッション",
"Set API key": "APIキーを設定",
"Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "モデルの温度(ランダム性)を設定します。Geminiは0~2、他は0~1。デフォルトは0.5。",
"Set temperature (randomness) of the model. Values range between 0 to 2 for Gemini, 0 to 1 for other models. Default is 0.5.": "モデルの温度(ランダム性)を設定します。Geminiは0~2、他は0~1です。初期値は0.5です。",
"Set the current API provider": "現在のAPIプロバイダーを設定します",
"Shutdown": "シャットダウン",
"Silent": "サイレント",
@@ -92,8 +90,8 @@
"Task Manager": "タスクマネージャー",
"Task description": "タスクの説明",
"Temperature must be between 0 and 2": "温度は0~2の間で指定してください",
"The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "エロ系 | 量が多くNSFW多数、品質はバラバラ",
"The popular one | Best quantity, but quality can vary wildly": "人気 | 量は最多品質はバラつきあり",
"The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向け | 量は多いが品質は様々・NSFW多数",
"The popular one | Best quantity, but quality can vary wildly": "人気 | 量は最多だが品質は様々",
"Thinking": "考え中",
"Translation goes here...": "ここに翻訳が表示されます...",
"Translator": "翻訳",
@@ -104,21 +102,17 @@
"Unknown Title": "不明なタイトル",
"View Markdown source": "Markdownソースを表示",
"Volume": "音量",
"Volume mixer": "音量ミキサー",
"Waifus only | Excellent quality, limited quantity": "美少女系のみ | 高品質量は少なめ",
"Volume mixer": "ボリュームミキサー",
"Waifus only | Excellent quality, limited quantity": "キャラクター | 高品質量は少なめ",
"Waiting for response...": "応答待ち...",
"Workspace": "ワークスペース",
"Set with /mode PROVIDER": "/mode PROVIDER で設定",
"Invalid API provider. Supported: \n-": "無効なAPIプロバイダー。対応: \n-",
"Unknown command:": "不明なコマンド:",
"Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "/keyでオンラインモデルを開始\nCtrl+Oでサイドバー展開\nCtrl+Pでサイドバーをウィンドウ化",
"The current API used. Endpoint:": "現在使用中のAPIのエンドポイント:",
"Type /key to get started with online models\nCtrl+O to expand the sidebar\nCtrl+P to detach sidebar into a window": "/key でオンラインモデルを開始\nCtrl+O でサイドバー展開\nCtrl+P でサイドバーをウィンドウ化",
"Provider set to": "プロバイダーを設定しました:",
"Invalid model. Supported: \n```": "無効なモデル。対応: \n```",
"That didn't work. Tips:\n- Check your tags and NSFW settings\n- If you don't have a tag in mind, type a page number": "うまくいきませんでした。ヒント:\n- タグやNSFW設定を確認\n- タグがなければページ番号を入力",
"Online | Google's model\nGives up-to-date information with search.": "オンライン | Googleのモデル\n検索で最新情報を取得します",
"Switched to search mode. Continue with the user's request.": "検索モードに切り替えました。リクエストを続行します。",
"Experimental | Online | Google's model\nCan do a little more but doesn't search quickly": "実験的 | オンライン | Googleのモデル\n少し多機能ですが、検索は遅めです",
"Settings": "設定",
"Save chat": "チャットを保存",
"Load chat": "チャットを読み込み",
@@ -137,12 +131,12 @@
"Material palette": "マテリアルパレット",
"Fidelity": "忠実度",
"Fruit Salad": "フルーツサラダ",
"Alternatively use /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgも利用可能",
"Fake screen rounding": "画面の角を疑似的に丸める",
"When not fullscreen": "全画面でない場合",
"Alternatively use /dark, /light, /img in the launcher": "ランチャーで /dark, /light, /img も使用できます",
"Fake screen rounding": "画面の角を丸める(疑似)",
"When not fullscreen": "フルスクリーンでない",
"Choose file": "ファイルを選択",
"Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "KonachanからランダムなSFW健全アニメ壁紙\n画像は~/Pictures/Wallpapersに保存されます",
"Be patient...": "しばらくお待ちください...",
"Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Konachanから健全アニメ壁紙をランダムで取得し、~/Pictures/Wallpapers に保存ます",
"Be patient...": "少々お待ちください",
"Decorations & Effects": "装飾と効果",
"Tonal Spot": "トーナルスポット",
"Shell windows": "シェルウィンドウ",
@@ -152,25 +146,24 @@
"Title bar": "タイトルバー",
"Transparency": "透明度",
"Expressive": "表現豊か",
"Yes": "はい",
"Yes": "表示",
"Enable": "有効化",
"Rainbow": "レインボー",
"Might look ass. Unsupported.": "見た目が悪くなる場合があります(未サポート",
"Might look ass. Unsupported.": "表示が崩れる可能性があります(非推奨",
"Monochrome": "モノクロ",
"Random: Konachan": "ランダム: Konachan",
"Center title": "タイトルを中央に",
"Neutral": "ニュートラル",
"Pick wallpaper image on your system": "システムから壁紙画像を選択",
"No": "いいえ",
"No": "非表示",
"AI": "AI",
"Local only": "ローカルのみ",
"Policies": "ポリシー",
"Weeb": "オタク向け",
"Closet": "クローゼット",
"Bar style": "バーのスタイル",
"Weeb": "アニメファン向け",
"Closet": "隠し",
"Show next time": "次回も表示する",
"Usage": "使用状況",
"Plain rectangle": "ただの四角形",
"Plain rectangle": "長方形",
"Useless buttons": "ダミーボタン",
"GitHub": "GitHub",
"Style & wallpaper": "スタイルと壁紙",
@@ -178,12 +171,11 @@
"Change any time later with /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgでいつでも変更可能",
"Keybinds": "キー割り当て",
"Float": "フローティング",
"Hug": "ハグ",
"Yooooo hi there": "やあ、こんにちは",
"illogical-impulse Welcome": "illogical-impulseへようこそ",
"Hug": "固定",
"illogical-impulse Welcome": "illogical-impulse へようこそ",
"Info": "情報",
"Volume limit": "音量制限",
"Prevents abrupt increments and restricts volume limit": "急な音量上昇を防ぎ、音量を制限します",
"Volume limit": "ボリューム制限",
"Prevents abrupt increments and restricts volume limit": "急な音量変化を防ぎ、音量の上限を設定します",
"Resources": "リソース",
"12h am/pm": "12時間(AM/PM",
"Base URL": "ベースURL",
@@ -194,32 +186,31 @@
"Battery": "バッテリー",
"Prefixes": "接頭辞",
"Emojis": "絵文字",
"Earbang protection": "急音防止",
"Earbang protection": "聴覚保護(急な大音量防止",
"Automatically suspends the system when battery is low": "バッテリー残量が少ないときに自動でスリープします",
"Automatic suspend": "自動スリープ",
"Suspend at": "スリープ開始残量(%",
"Max allowed increase": "最大許容増加幅",
"Max allowed increase": "音量の最大増加幅",
"Web search": "ウェブ検索",
"Polling interval (ms)": "ポーリング間隔(ms",
"Clipboard": "クリップボード",
"Low warning": "バッテリー低下の警告",
"24h": "24時間",
"Use Levenshtein distance-based algorithm instead of fuzzy": "ファジー検索の代わりにレーベンシュタイン距離ベースのアルゴリズムを使用",
"Use Levenshtein distance-based algorithm instead of fuzzy": "あいまい検索の代わりにレーベンシュタイン距離アルゴリズムを使用",
"System prompt": "システムプロンプト",
"12h AM/PM": "12時間(AM/PM",
"Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)": "タイプミスが多い場合便利ですが、結果が変になることもあり、略語には対応しない場合があります\n(例: \"GIMP\"でペイントソフトが出ないことも)",
"Could be better if you make a ton of typos,\nbut results can be weird and might not work with acronyms\n(e.g. \"GIMP\" might not give you the paint program)":"入力ミスが多い場合便利ですが、結果が意図しないものになったり、略語(例:GIMP)が正しく検索できないことがあります",
"Critical warning": "重大な警告",
"User agent (for services that require it)": "ユーザーエージェント(必要なサービス用)",
"Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "これらの領域は、画像や画面の一部など、一定のまとまりを持つ箇所を指します。\n常に正確とは限りません。\nこれはローカルで実行される画像処理アルゴリズムによるもので、AIは使用していません。",
"Note: turning off can hurt readability": "オフにすると可読性が下がる場合があります",
"Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.": "これらの領域は、画像や画面の一部など、まとまりのある部分を指します。\n必ずしも正確ではありません。\nAIではなく、ローカルで実行される画像処理アルゴリズムによって検出されます。",
"Note: turning off can hurt readability": "注意: オフにすると可読性が損なわれる場合があります",
"Workspaces shown": "表示中のワークスペース",
"Dark/Light toggle": "ダーク/ライト切替",
"Dock": "ドック",
"Weather": "天気",
"Pinned on startup": "起動時にピン留め",
"Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント: アイコンを隠して常に数字を表示すると、クラシックなillogical-impulse体験になります",
"Appearance": "外観",
"Always show numbers": "常に数字を表示",
"Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント: アイコンを非表示にして番号を常に表示すると、クラシックなillogical-impulseの使用感を体験できます",
"Always show numbers": "数字を常に表示",
"Buttons": "ボタン",
"Keyboard toggle": "キーボード切替",
"Scale (%)": "スケール(%)",
@@ -232,8 +223,8 @@
"Show app icons": "アプリアイコンを表示",
"Workspaces": "ワークスペース",
"Columns": "列数",
"On-screen display": "オンスクリーン表示",
"Screen snip": "画面切り取り",
"On-screen display": "画面表示",
"Screen snip": "画面切り抜き",
"Mic toggle": "マイク切替",
"Hover to reveal": "ホバーで表示",
"Bar": "バー",
@@ -247,7 +238,7 @@
"Distro": "ディストリビューション",
"Privacy Policy": "プライバシーポリシー",
"Documentation": "ドキュメント",
"Shell & utilities theming must also be enabled": "シェルとユーティリティのテーマも有効にする必要があります",
"Shell & utilities theming must also be enabled": "シェルとユーティリティのテーマ設定も有効にする必要があります",
"illogical-impulse": "illogical-impulse",
"Donate": "寄付",
"Terminal": "ターミナル",
@@ -257,7 +248,7 @@
"Issues": "課題",
"Drag or click a region • LMB: Copy • RMB: Edit": "領域をドラッグまたはクリック • 左クリック: コピー • 右クリック: 編集",
"Current model: %1\nSet it with %2model MODEL": "現在のモデル: %1\n%2model MODELで設定",
"Message the model... \"%1\" for commands": "モデルにメッセージ... コマンドは「%1」",
"Message the model... \"%1\" for commands": "モデルにメッセージを送信... コマンドは「%1」",
"No API key set for %1": "%1のAPIキーが設定されていません",
"Loaded the following system prompt\n\n---\n\n%1": "次のシステムプロンプトを読み込みました\n\n---\n\n%1",
"%1 | Right-click to configure": "%1 | 右クリックで設定",
@@ -266,11 +257,9 @@
"Current API endpoint: %1\nSet it with %2mode PROVIDER": "現在のAPIエンドポイント: %1\n%2mode PROVIDERで設定",
"Go to source (%1)": "ソースを開く(%1",
"Temperature set to %1": "温度を%1に設定",
"To set an API key, pass it with the command\n\nTo view the key, pass \"get\" with the command<br/>\n\n### For %1:\n\n**Link**: %2\n\n%3": "APIキーを設定するにはコマンドで渡してください\n\nキーを表示するには「get」を指定してください<br/>\n\n### %1の場合:\n\n**リンク**: %2\n\n%3",
"Enter tags, or \"%1\" for commands": "タグを入力、またはコマンドは「%1」",
"%1 queries pending": "%1件のクエリが保留中",
"API key:\n\n```txt\n%1\n```": "APIキー:\n\n```txt\n%1\n```",
"Uptime: %1": "稼働時間: %1",
"%1 Safe Storage": "%1 セーフストレージ",
"%1 does not require an API key": "%1はAPIキー不要です",
"Temperature: %1": "温度: %1",
@@ -288,19 +277,19 @@
"Critically low battery": "バッテリー残量が非常に少ない",
"Select output device": "出力デバイスを選択",
"Code saved to file": "コードをファイルに保存しました",
"Online models disallowed\n\nControlled by `policies.ai` config option": "オンラインモデルは許可されていません\n\n`policies.ai`設定で制御されています",
"Online models disallowed\n\nControlled by `policies.ai` config option": "オンラインモデルは許可されていません\n\n`policies.ai` 設定で管理されています",
"Scroll to change volume": "スクロールで音量調整",
"Elements": "要素",
"%1 • %2 tasks": "%1 • %2件のタスク",
"Download complete": "ダウンロード完了",
"Please charge!\nAutomatic suspend triggers at %1": "充電してください!\n%1で自動スリープが作動します",
"Please charge!\nAutomatic suspend triggers at %1": "充電してください!\n%1で自動的にサスペンドします",
"Cloudflare WARP": "Cloudflare WARP",
"Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)",
"Scroll to change brightness": "スクロールで明るさ調整",
"Connection failed. Please inspect manually with the <tt>warp-cli</tt> command": "接続に失敗しました。<tt>warp-cli</tt>コマンドで手動確認してください",
"Connection failed. Please inspect manually with the <tt>warp-cli</tt> command": "接続に失敗しました。<tt>warp-cli</tt> コマンドで手動確認してください",
"Select input device": "入力デバイスを選択",
"Registration failed. Please inspect manually with the <tt>warp-cli</tt> command": "登録に失敗しました。<tt>warp-cli</tt>コマンドで手動確認してください",
"Consider plugging in your device": "電源接続してください",
"Registration failed. Please inspect manually with the <tt>warp-cli</tt> command": "登録に失敗しました。<tt>warp-cli</tt> コマンドで手動確認してください",
"Consider plugging in your device": "電源接続することを推奨します",
"Low battery": "バッテリー残量低下",
"Saved to %1": "%1に保存しました",
"Sunset": "日没",
@@ -316,23 +305,128 @@
"Fully charged": "充電完了",
"Charging:": "充電中:",
"Discharging:": "放電中:",
"Uptime:": "稼働時間:",
"Upcoming Tasks:": "今後のタスク:",
"No pending tasks": "保留中のタスクなし",
"... and %1 more": "...他%1件",
"Memory Usage": "メモリ使用量",
"Used:": "使用済み:",
"Free:": "空き:",
"Total:": "合計:",
"Usage:": "使用状況:",
"Swap Usage": "スワップ使用量",
"Swap:": "スワップ:",
"Not configured": "未設定",
"CPU Usage": "CPU使用率",
"Current:": "現在:",
"Load:": "負荷:",
"High": "高",
"Medium": "中",
"Low": "低",
"System Resource": "システムリソース"
"Use the system file picker instead": "システム標準のファイル選択ツールを使用",
"Tint icons": "アイコンに色付けする",
"Connect to Wi-Fi": "Wi-Fiに接続",
"Invalid arguments. Must provide `command`.": "無効な引数です。`command`を指定してください。",
"System uptime:": "システム稼働時間:",
"Gives the model search capabilities (immediately)": "モデルにすぐに検索能力を与えます",
"Click to toggle light/dark mode (applied when wallpaper is chosen)": "クリックでライト/ダークモード切替(壁紙選択時に適用されます)",
"**Pricing**: Free tier available with limited rates. See https://docs.github.com/en/billing/concepts/product-billing/github-models\n\n**Instructions**: Generate a GitHub personal access token with Models permission, then set as API key here\n\n**Note**: To use this you will have to set the temperature parameter to 1": "**料金**: 制限付きの無料枠があります。詳細は https://docs.github.com/en/billing/concepts/product-billing/github-models を参照\n\n**手順**: Modelsアクセス権を持つGitHub個人アクセストークンを生成し、ここでAPIキーとして設定してください\n\n**注意**: 使用するには温度パラメータを1に設定する必要があります",
"Depends on workspace": "ワークスペースに依存",
"Hi there! First things first...": "こんにちは!まずは始めましょう...",
"Refreshing (manually triggered)": "更新中(手動で開始)",
"Always": "常に",
"No API key\nSet it with /key YOUR_API_KEY": "APIキーがありません\n/key YOUR_API_KEYで設定してください",
"Usage: %1load CHAT_NAME": "使用法: %1load チャット名",
"Sidebars": "サイドバー",
"Search wallpapers": "壁紙を検索",
"When this is off you'll have to click": "オフの場合、クリックで表示します",
"Depends on sidebars": "サイドバーに依存",
"Incorrect password": "パスワードが間違っています",
"Current tool: %1\nSet it with %2tool TOOL": "現在のツール: %1\n%2tool TOOLで設定",
"Overall appearance": "全体の外観",
"To Do:": "やること:",
"Region height": "領域の高さ",
"Auto (System)": "自動(システム)",
"Place the corners to trigger at the bottom": "トリガーとなる角を下部に配置",
"Shell conflicts killer": "シェルの競合解消",
"Enter password": "パスワードを入力",
"☕ Break: %1 minutes": "☕ 休憩: %1分",
"Reset": "リセット",
"Connect": "接続",
"Tint app icons": "アプリアイコンに淡い色付けをする",
"Bar layout": "バーのレイアウト",
"Conflicts with the shell's notification implementation": "シェルの通知機能と競合することがあります",
"Corner open": "コーナー起動",
"🌿 Long break: %1 minutes": "🌿 長い休憩: %1分",
"Reject": "拒否",
"Command rejected by user": "コマンドはユーザーによって拒否されました",
"Start": "開始",
"Brightness and volume": "明るさ・音量",
"Corner style": "角のスタイル",
"Total token count\nInput: %1\nOutput: %2": "トークン数合計\n入力: %1\n出力: %2",
"No active player": "アクティブなプレーヤーがありません",
"Performance Profile toggle": "パフォーマンスプロファイル切替",
"Timer": "タイマー",
"Conflicts with the shell's system tray implementation": "シェルのシステムトレイ機能と競合することがあります",
"Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls": "オンライン | %1のモデル | 高速で反応が良く、整形された回答が得られます。欠点: あまり積極的でない場合があり、存在しない関数を提案することがあります",
"Welcome app": "ようこそアプリ",
"Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput.": "オンライン | Googleモデル\nコスト効率と高スループットに最適化されたGemini 2.5 Flashモデル。",
"Wallpaper parallax": "壁紙パララックス効果",
"Place at the bottom": "下部に配置",
"Invalid tool. Supported tools:\n- %1": "無効なツールです。対応ツール:\n- %1",
"Password": "パスワード",
"Details": "詳細",
"Edit directory": "ディレクトリを編集",
"Language": "言語",
"Visualize region": "領域を可視化",
"Enjoy! You can reopen the welcome app any time with <tt>Super+Shift+Alt+/</tt>. To open the settings app, hit <tt>Super+I</tt>": "お楽しみください!ウェルカムアプリは<tt>Super+Shift+Alt+/</tt>でいつでも開けます。設定アプリを開くには<tt>Super+I</tt>を押してください",
"Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks.": "オンライン | Googleモデル\nコーディングや複雑な推論タスクに優れた、Googleの最先端多目的モデル。",
"When enabled keeps the content of the right sidebar loaded to reduce the delay when opening,\nat the cost of around 15MB of consistent RAM usage. Delay significance depends on your system's performance.\nUsing a custom kernel like linux-cachyos might help": "有効にすると右サイドバーのコンテンツを常に読み込んでおくことで、表示遅延を短縮します。\n代償として約15MBのメモリを継続的に使用します。遅延の度合いはシステム性能に依存します。\nlinux-cachyosのようなカスタムカーネルの使用が効果的な場合があります。",
"Place at bottom": "下部に配置",
"Tool set to: %1": "ツールを設定: %1",
"Set the tool to use for the model.": "モデルが使用するツールを設定します。",
"Make sure your player has MPRIS support\nor try turning off duplicate player filtering": "お使いのプレイヤーがMPRISをサポートしているか確認するか、\n重複プレイヤーのフィルタリングを無効にしてみてください",
"Select the language for the user interface.\n\"Auto\" will use your system's locale.": "インターフェース言語を選択します。\n「自動」を選ぶとシステムのロケールが使用されます。",
"Value scroll": "スクロールで音量・明るさを調整",
"There might be a download in progress": "ダウンロードが進行中のようです",
"Approve": "承認",
"Tray": "トレイ",
"**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key": "**手順**: Mistralアカウントにログインし、サイドバーのKeysに進み、Create new keyをクリックしてください",
"Pomodoro": "ポモドーロ",
"Language setting saved. Please restart Quickshell (Ctrl+Super+R) to apply the new language.": "言語設定を保存しました。新しい言語を適用するにはQuickshellを再起動してください(Ctrl+Super+R)。",
"Feels like %1": "体感温度 %1",
"Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed": "コマンド、設定編集、検索が可能。\n検索モードへの切替が必要な場合は追加の対話が必要です",
"Resume": "再開",
"Preferred wallpaper zoom (%)": "壁紙の拡大率(%",
"Disable tools": "ツールをオフ",
"Night Light | Right-click to toggle Auto mode": "ナイトライト | 右クリックで自動モード切替",
"Online | Google's model\nFast, can perform searches for up-to-date information": "オンライン | Googleモデル\n高速で、最新情報を検索できます",
"Hover to trigger": "ホバーでトリガー",
"Keep right sidebar loaded": "右サイドバーを常に読み込む",
"Kill conflicting programs?": "競合するプログラムを終了しますか?",
"Pick a wallpaper": "壁紙を選択",
"Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers": "オンライン | Googleモデル\n旧モデルより低速ですが、より高品質な回答が期待できる新モデル",
"Hit \"/\" to search": "「/」で検索",
"Config file": "設定ファイル",
"Attach a file. Only works with Gemini.": "ファイルを添付 (Geminiでのみ利用可能)",
"To set an API key, pass it with the %4 command\n\nTo view the key, pass \"get\" with the command<br/>\n\n### For %1:\n\n**Link**: %2\n\n%3": "APIキーを設定するには、%4コマンドを使用してください\n\nキーを確認するには、コマンドに「get」を付けてください<br/>\n\n### %1について:\n\n**リンク**: %2\n\n%3",
"Thought": "思考",
"Long break": "長い休憩",
"Temperature\nChange with /temp VALUE": "温度\n/temp 値で変更",
"Usage: %1tool TOOL_NAME": "使用法: %1tool ツール名",
"Pause": "一時停止",
"Allows you to open sidebars by clicking or hovering screen corners regardless of bar position": "バーの配置に関わらず、画面コーナーのクリックまたはホバーでサイドバーを開けるようにします",
"EasyEffects | Right-click to configure": "EasyEffects | 右クリックで設定",
"Up %1": "%1稼働中",
"Place at the bottom/right": "下部/右側に配置",
"Focus": "集中",
"Stopwatch": "ストップウォッチ",
"Interface Language": "インターフェース言語",
"Memory usage": "メモリ使用量",
"Automatically hide": "自動的に隠す",
"Break": "休憩",
"Your package manager is running": "パッケージマネージャーが実行中です",
"API key is set\nChange with /key YOUR_API_KEY": "APIキーが設定済み\n/key YOUR_API_KEYで変更できます",
"Open the shell config file.\nIf the button doesn't work or doesn't open in your favorite editor,\nyou can manually open ~/.config/illogical-impulse/config.json": "シェルの設定ファイルを開きます。\nボタンが機能しない、または任意のエディタで開かない場合は、\n~/.config/illogical-impulse/config.json を手動で開いてください",
"CPU usage": "CPU使用率",
"Swap usage": "スワップ使用量",
"Usage: %1save CHAT_NAME": "使用法: %1save チャット名",
"🔴 Focus: %1 minutes": "🔴 集中: %1分",
"Lap": "ラップ",
"Horizontal": "水平",
"Region width": "領域の幅",
"Vertical": "垂直"
}
+5
View File
@@ -8,3 +8,8 @@ materialyoucolor
libsass
material-color-utilities
setproctitle
click
loguru
pycairo
pygobject
tqdm
+12
View File
@@ -4,8 +4,12 @@ build==1.2.2.post1
# via -r scriptdata/requirements.in
cffi==1.17.1
# via pywayland
click==8.2.1
# via -r scriptdata/requirements.in
libsass==0.23.0
# via -r scriptdata/requirements.in
loguru==0.7.3
# via -r scriptdata/requirements.in
material-color-utilities==0.2.1
# via -r scriptdata/requirements.in
materialyoucolor==2.0.10
@@ -22,8 +26,14 @@ pillow==11.1.0
# material-color-utilities
psutil==6.1.1
# via -r scriptdata/requirements.in
pycairo==1.28.0
# via
# -r scriptdata/requirements.in
# pygobject
pycparser==2.22
# via cffi
pygobject==3.52.3
# via -r scriptdata/requirements.in
pyproject-hooks==1.2.0
# via build
pywayland==0.4.18
@@ -34,5 +44,7 @@ setuptools==80.9.0
# via setuptools-scm
setuptools-scm==8.1.0
# via -r scriptdata/requirements.in
tqdm==4.67.1
# via -r scriptdata/requirements.in
wheel==0.45.1
# via -r scriptdata/requirements.in