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 = Alt ,XF86AudioMute, exec, wpctl set-mute @DEFAULT_SOURCE@ toggle # [hidden]
bindl = ,XF86AudioMicMute, 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] 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 bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $qsConfig & # Restart widgets
##! Utilities ##! Utilities
+2
View File
@@ -134,8 +134,10 @@ layerrule = animation slide, quickshell:verticalBar
layerrule = animation fade, quickshell:screenCorners layerrule = animation fade, quickshell:screenCorners
layerrule = animation slide right, quickshell:sidebarRight layerrule = animation slide right, quickshell:sidebarRight
layerrule = animation slide left, quickshell:sidebarLeft layerrule = animation slide left, quickshell:sidebarLeft
layerrule = animation slide top, quickshell:wallpaperSelector
layerrule = animation slide bottom, quickshell:osk layerrule = animation slide bottom, quickshell:osk
layerrule = animation slide bottom, quickshell:dock layerrule = animation slide bottom, quickshell:dock
layerrule = animation slide bottom, quickshell:cheatsheet
layerrule = blur, quickshell:session layerrule = blur, quickshell:session
layerrule = noanim, quickshell:session layerrule = noanim, quickshell:session
layerrule = ignorealpha 0, quickshell:session layerrule = ignorealpha 0, quickshell:session
+1
View File
@@ -17,6 +17,7 @@ Singleton {
property bool osdVolumeOpen: false property bool osdVolumeOpen: false
property bool oskOpen: false property bool oskOpen: false
property bool overviewOpen: false property bool overviewOpen: false
property bool wallpaperSelectorOpen: false
property bool screenLocked: false property bool screenLocked: false
property bool screenLockContainsCharacters: false property bool screenLockContainsCharacters: false
property bool screenUnlockFailed: false property bool screenUnlockFailed: false
@@ -29,7 +29,7 @@ Variants {
// Hide when fullscreen // Hide when fullscreen
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace=>workspace.monitor && workspace.monitor.name == monitor.name) 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 visible: GlobalStates.screenLocked || (!(activeWorkspaceWithFullscreen != undefined)) || !Config?.options.background.hideWhenFullscreen
// Workspaces // Workspaces
@@ -278,9 +278,9 @@ Variants {
} }
color: bgRoot.colText color: bgRoot.colText
style: Text.Raised style: Text.Raised
visible: Config.options.background.mantra !== "" visible: Config.options.background.quote !== ""
styleColor: Appearance.colors.colShadow styleColor: Appearance.colors.colShadow
text: Config.options.background.mantra text: Config.options.background.quote
} }
} }
@@ -2,7 +2,6 @@ import "./weather"
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Bluetooth
import Quickshell.Services.UPower import Quickshell.Services.UPower
import qs import qs
import qs.services import qs.services
@@ -307,10 +306,7 @@ Item { // Bar content region
color: rightSidebarButton.colText color: rightSidebarButton.colText
} }
MaterialSymbol { MaterialSymbol {
readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
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"
iconSize: Appearance.font.pixelSize.larger iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText color: rightSidebarButton.colText
} }
@@ -216,8 +216,11 @@ Item {
anchors.centerIn: parent anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) font {
text: `${button.workspaceValue}` 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 elide: Text.ElideRight
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ? color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onPrimary :
@@ -350,6 +350,10 @@ Singleton {
property real baseVerticalBarWidth: 46 property real baseVerticalBarWidth: 46
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ? property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth (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" 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 real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
property bool enableSidebar: true property bool enableSidebar: true
} }
property string mantra: "" property string quote: ""
property bool hideWhenFullscreen: true property bool hideWhenFullscreen: true
} }
@@ -175,6 +175,8 @@ Singleton {
property bool showAppIcons: true property bool showAppIcons: true
property bool alwaysShowNumbers: false property bool alwaysShowNumbers: false
property int showNumberDelay: 300 // milliseconds 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 JsonObject weather: JsonObject {
property bool enable: false property bool enable: false
@@ -8,11 +8,16 @@ import Quickshell
Singleton { Singleton {
// XDG Dirs, with "file://" // 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 config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[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 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://" // Other dirs used by the shell, without "file://"
property string assetsPath: Quickshell.shellPath("assets") property string assetsPath: Quickshell.shellPath("assets")
@@ -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(); 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 * Removes the file extension from a file path or name
* @param {string} str * @param {string} str
@@ -38,4 +52,18 @@ Singleton {
} }
return trimmed; 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;
return str.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;').replace(/'/g, '&#39;'); 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
import qs.modules.common.functions
import qs.modules.common.widgets
import QtQuick import QtQuick
/** /**
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview * Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
*/ */
RippleButton { RippleButton {
id: button id: root
property string buttonText property string buttonText
implicitHeight: 30 padding: 14
implicitWidth: buttonTextWidget.implicitWidth + 15 * 2 implicitHeight: 36
implicitWidth: buttonTextWidget.implicitWidth + padding * 2
buttonRadius: Appearance?.rounding.full ?? 9999 buttonRadius: Appearance?.rounding.full ?? 9999
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F" property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96" 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 { contentItem: StyledText {
id: buttonTextWidget id: buttonTextWidget
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: 15 anchors.leftMargin: root.padding
anchors.rightMargin: 15 anchors.rightMargin: root.padding
text: buttonText text: buttonText
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
font.pixelSize: Appearance?.font.pixelSize.small ?? 12 font.pixelSize: Appearance?.font.pixelSize.small ?? 12
color: button.enabled ? button.colEnabled : button.colDisabled color: root.enabled ? root.colEnabled : root.colDisabled
Behavior on color { Behavior on color {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) 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 import QtQuick.Controls
/** /**
* Material 3 styled TextArea (filled style) * Material 3 styled TextField (filled style)
* https://m3.material.io/components/text-fields/overview * https://m3.material.io/components/text-fields/overview
* Note: We don't use NativeRendering because it makes the small placeholder text look weird * Note: We don't use NativeRendering because it makes the small placeholder text look weird
*/ */
TextArea { TextField {
id: root id: root
Material.theme: Material.System Material.theme: Material.System
Material.accent: Appearance.m3colors.m3primary Material.accent: Appearance.m3colors.m3primary
Material.primary: Appearance.m3colors.m3primary Material.primary: Appearance.m3colors.m3primary
Material.background: Appearance.m3colors.m3surface Material.background: Appearance.m3colors.m3surface
Material.foreground: Appearance.m3colors.m3onSurface Material.foreground: Appearance.m3colors.m3onSurface
Material.containerStyle: Material.Filled Material.containerStyle: Material.Outlined
renderType: Text.QtRendering renderType: Text.QtRendering
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer
placeholderTextColor: Appearance.m3colors.m3outline placeholderTextColor: Appearance.m3colors.m3outline
clip: true
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 { font {
family: Appearance?.font.family.main ?? "sans-serif" family: Appearance?.font.family.main ?? "sans-serif"
@@ -49,4 +29,11 @@ TextArea {
hintingPreference: Font.PreferFullHinting hintingPreference: Font.PreferFullHinting
} }
wrapMode: TextEdit.Wrap wrapMode: TextEdit.Wrap
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
hoverEnabled: true
cursorShape: Qt.IBeamCursor
}
} }
@@ -12,6 +12,7 @@ Button {
id: root id: root
property bool toggled property bool toggled
property string buttonText property string buttonText
property bool pointingHandCursor: true
property real buttonRadius: Appearance?.rounding?.small ?? 4 property real buttonRadius: Appearance?.rounding?.small ?? 4
property real buttonRadiusPressed: buttonRadius property real buttonRadiusPressed: buttonRadius
property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
@@ -58,7 +59,7 @@ Button {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: root.pointingHandCursor ? Qt.PointingHandCursor : Qt.ArrowCursor
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: (event) => { onPressed: (event) => {
if(event.button === Qt.RightButton) { if(event.button === Qt.RightButton) {
@@ -5,7 +5,7 @@ Item {
id: root id: root
enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight } 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 int implicitSize: 25
property color color: "#000000" 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 int dragIndex: -1
property real dragDistance: 0 property real dragDistance: 0
property bool popin: true property bool popin: true
property bool animateAppearance: true
property bool animateMovement: false
// Accumulated scroll destination so wheel deltas stack while animating // Accumulated scroll destination so wheel deltas stack while animating
property real scrollTargetY: 0 property real scrollTargetY: 0
@@ -66,17 +68,17 @@ ListView {
} }
add: Transition { add: Transition {
animations: [ animations: animateAppearance ? [
Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
properties: popin ? "opacity,scale" : "opacity", properties: popin ? "opacity,scale" : "opacity",
from: 0, from: 0,
to: 1, to: 1,
}), }),
] ] : []
} }
addDisplaced: Transition { addDisplaced: Transition {
animations: [ animations: animateAppearance ? [
Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
property: "y", property: "y",
}), }),
@@ -84,46 +86,46 @@ ListView {
properties: popin ? "opacity,scale" : "opacity", properties: popin ? "opacity,scale" : "opacity",
to: 1, to: 1,
}), }),
] ] : []
} }
// displaced: Transition { displaced: Transition {
// animations: [ animations: root.animateMovement ? [
// Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
// property: "y", property: "y",
// }), }),
// Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
// properties: "opacity,scale", properties: "opacity,scale",
// to: 1, to: 1,
// }), }),
// ] ] : []
// } }
// move: Transition { move: Transition {
// animations: [ animations: root.animateMovement ? [
// Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
// property: "y", property: "y",
// }), }),
// Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
// properties: "opacity,scale", properties: "opacity,scale",
// to: 1, to: 1,
// }), }),
// ] ] : []
// } }
// moveDisplaced: Transition { moveDisplaced: Transition {
// animations: [ animations: root.animateMovement ? [
// Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
// property: "y", property: "y",
// }), }),
// Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
// properties: "opacity,scale", properties: "opacity,scale",
// to: 1, to: 1,
// }), }),
// ] ] : []
// } }
remove: Transition { remove: Transition {
animations: [ animations: animateAppearance ? [
Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
property: "x", property: "x",
to: root.width + root.removeOvershoot, to: root.width + root.removeOvershoot,
@@ -132,12 +134,12 @@ ListView {
property: "opacity", property: "opacity",
to: 0, to: 0,
}) })
] ] : []
} }
// This is movement when something is removed, not removing animation! // This is movement when something is removed, not removing animation!
removeDisplaced: Transition { removeDisplaced: Transition {
animations: [ animations: animateAppearance ? [
Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
property: "y", property: "y",
}), }),
@@ -145,6 +147,6 @@ ListView {
properties: "opacity,scale", properties: "opacity,scale",
to: 1, 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. * Does not include visual layout, but includes the easily neglected colors.
*/ */
TextInput { TextInput {
color: Appearance.colors.colOnLayer1
renderType: Text.NativeRendering renderType: Text.NativeRendering
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer selectionColor: Appearance.colors.colSecondaryContainer
@@ -10,7 +10,7 @@ import qs.modules.common.widgets
Item { Item {
id: root id: root
property real padding: 6 property real padding: 8
property alias colBackground: background.color property alias colBackground: background.color
default property alias data: toolbarLayout.data default property alias data: toolbarLayout.data
implicitWidth: background.implicitWidth implicitWidth: background.implicitWidth
@@ -23,13 +23,14 @@ Item {
Rectangle { Rectangle {
id: background id: background
anchors.centerIn: parent anchors.centerIn: parent
color: Appearance.colors.colLayer2 color: Appearance.m3colors.m3surfaceContainer // Needs to be opaque
implicitHeight: toolbarLayout.implicitHeight + root.padding * 2 implicitHeight: toolbarLayout.implicitHeight + root.padding * 2
implicitWidth: toolbarLayout.implicitWidth + root.padding * 2 implicitWidth: toolbarLayout.implicitWidth + root.padding * 2
radius: Appearance.rounding.full radius: Appearance.rounding.full
RowLayout { RowLayout {
id: toolbarLayout id: toolbarLayout
spacing: 4
anchors { anchors {
fill: parent fill: parent
margins: root.padding margins: root.padding
@@ -4,7 +4,5 @@ import qs.modules.common
RippleButton { RippleButton {
Layout.fillHeight: true Layout.fillHeight: true
Layout.topMargin: 2
Layout.bottomMargin: 2
buttonRadius: Appearance.rounding.full buttonRadius: Appearance.rounding.full
} }
@@ -10,8 +10,6 @@ TextField {
property alias colBackground: background.color property alias colBackground: background.color
Layout.fillHeight: true Layout.fillHeight: true
Layout.topMargin: 2
Layout.bottomMargin: 2
implicitWidth: 200 implicitWidth: 200
padding: 10 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() { function tryUnlock() {
if (currentText === "") return;
root.unlockInProgress = true; root.unlockInProgress = true;
pam.start(); pam.start();
} }
@@ -42,12 +40,6 @@ Scope {
PamContext { PamContext {
id: pam 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 // pam_unix will ask for a response for the password prompt
onPamMessage: { onPamMessage: {
if (this.responseRequired) { if (this.responseRequired) {
@@ -133,7 +133,7 @@ MouseArea {
id: confirmButton id: confirmButton
implicitWidth: height implicitWidth: height
toggled: true toggled: true
enabled: !root.context.unlockInProgress && root.context.currentText.length > 0 enabled: !root.context.unlockInProgress
colBackgroundToggled: Appearance.colors.colPrimary colBackgroundToggled: Appearance.colors.colPrimary
onClicked: root.context.tryUnlock() onClicked: root.context.tryUnlock()
@@ -1 +0,0 @@
auth required pam_unix.so
@@ -40,37 +40,34 @@ Scope {
} }
return ( return (
// Remove unecessary native buses from browsers if there's plasma integration // 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.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.chromium')) &&
// playerctld just copies other buses and we don't need duplicates // playerctld just copies other buses and we don't need duplicates
!player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') && !player.dbusName?.startsWith('org.mpris.MediaPlayer2.playerctld') &&
// Non-instance mpd bus // 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) { function filterDuplicatePlayers(players) {
let filtered = []; let filtered = [];
let used = new Set(); let used = new Set();
for (let i = 0; i < players.length; ++i) { for (let i = 0; i < players.length; ++i) {
if (used.has(i)) continue; if (used.has(i))
continue;
let p1 = players[i]; let p1 = players[i];
let group = [i]; let group = [i];
// Find duplicates by trackTitle prefix // Find duplicates by trackTitle prefix
for (let j = i + 1; j < players.length; ++j) { for (let j = i + 1; j < players.length; ++j) {
let p2 = players[j]; let p2 = players[j];
if (p1.trackTitle && p2.trackTitle && 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)) {
(p1.trackTitle.includes(p2.trackTitle)
|| p2.trackTitle.includes(p1.trackTitle))
|| (p1.position - p2.position <= 2 && p1.length - p2.length <= 2)) {
group.push(j); group.push(j);
} }
} }
// Pick the one with non-empty trackArtUrl, or fallback to the first // 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); 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]); filtered.push(players[chosenIdx]);
group.forEach(idx => used.add(idx)); group.forEach(idx => used.add(idx));
@@ -133,6 +130,16 @@ Scope {
item: playerColumnLayout item: playerColumnLayout
} }
HyprlandFocusGrab {
windows: [mediaControlsRoot]
active: mediaControlsLoader.active
onCleared: () => {
if (!active) {
GlobalStates.mediaControlsOpen = false;
}
}
}
ColumnLayout { ColumnLayout {
id: playerColumnLayout id: playerColumnLayout
anchors.fill: parent anchors.fill: parent
@@ -148,6 +155,43 @@ Scope {
visualizerPoints: root.visualizerPoints visualizerPoints: root.visualizerPoints
implicitWidth: widgetWidth implicitWidth: widgetWidth
implicitHeight: widgetHeight 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 { function toggle(): void {
mediaControlsLoader.active = !mediaControlsLoader.active; mediaControlsLoader.active = !mediaControlsLoader.active;
if(mediaControlsLoader.active) Notifications.timeoutAll(); if (mediaControlsLoader.active)
Notifications.timeoutAll();
} }
function close(): void { function close(): void {
@@ -196,5 +241,4 @@ Scope {
GlobalStates.mediaControlsOpen = false; GlobalStates.mediaControlsOpen = false;
} }
} }
} }
@@ -22,6 +22,7 @@ Item { // Player instance
property list<real> visualizerPoints: [] property list<real> visualizerPoints: []
property real maxVisualizerValue: 1000 // Max value in the data points property real maxVisualizerValue: 1000 // Max value in the data points
property int visualizerSmoothing: 2 // Number of points to average for smoothing property int visualizerSmoothing: 2 // Number of points to average for smoothing
property real radius
component TrackChangeButton: RippleButton { component TrackChangeButton: RippleButton {
implicitWidth: 24 implicitWidth: 24
@@ -107,7 +108,7 @@ Item { // Player instance
anchors.fill: parent anchors.fill: parent
anchors.margins: Appearance.sizes.elevationMargin anchors.margins: Appearance.sizes.elevationMargin
color: blendedColors.colLayer0 color: blendedColors.colLayer0
radius: root.popupRounding radius: playerController.radius
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
@@ -141,7 +142,7 @@ Item { // Player instance
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
color: ColorUtils.transparentize(blendedColors.colLayer0, 0.3) 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 Cliphist.fuzzyQuery(searchString).map(entry => {
return { return {
cliphistRawString: entry, cliphistRawString: entry,
name: entry.replace(/^\s*\S+\s+/, ""), name: StringUtils.cleanCliphistEntry(entry),
clickActionName: "", clickActionName: "",
type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`,
execute: () => { execute: () => {
@@ -126,7 +126,7 @@ Scope {
// Hide when fullscreen // Hide when fullscreen
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace => workspace.monitor && workspace.monitor.name == monitor.name) 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 property bool fullscreen: activeWorkspaceWithFullscreen != undefined
CornerPanelWindow { CornerPanelWindow {
@@ -49,7 +49,7 @@ ContentPage {
} }
ContentSection { ContentSection {
title: Translation.tr("AI") title: Translation.tr("AI")
MaterialTextField { MaterialTextArea {
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: Translation.tr("System prompt") placeholderText: Translation.tr("System prompt")
text: Config.options.ai.systemPrompt text: Config.options.ai.systemPrompt
@@ -115,7 +115,7 @@ ContentPage {
ContentSection { ContentSection {
title: Translation.tr("Networking") title: Translation.tr("Networking")
MaterialTextField { MaterialTextArea {
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: Translation.tr("User agent (for services that require it)") placeholderText: Translation.tr("User agent (for services that require it)")
text: Config.options.networking.userAgent text: Config.options.networking.userAgent
@@ -159,7 +159,7 @@ ContentPage {
ConfigRow { ConfigRow {
uniform: true uniform: true
MaterialTextField { MaterialTextArea {
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: Translation.tr("Action") placeholderText: Translation.tr("Action")
text: Config.options.search.prefix.action text: Config.options.search.prefix.action
@@ -168,7 +168,7 @@ ContentPage {
Config.options.search.prefix.action = text; Config.options.search.prefix.action = text;
} }
} }
MaterialTextField { MaterialTextArea {
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: Translation.tr("Clipboard") placeholderText: Translation.tr("Clipboard")
text: Config.options.search.prefix.clipboard text: Config.options.search.prefix.clipboard
@@ -177,7 +177,7 @@ ContentPage {
Config.options.search.prefix.clipboard = text; Config.options.search.prefix.clipboard = text;
} }
} }
MaterialTextField { MaterialTextArea {
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: Translation.tr("Emojis") placeholderText: Translation.tr("Emojis")
text: Config.options.search.prefix.emojis text: Config.options.search.prefix.emojis
@@ -190,7 +190,7 @@ ContentPage {
} }
ContentSubsection { ContentSubsection {
title: Translation.tr("Web search") title: Translation.tr("Web search")
MaterialTextField { MaterialTextArea {
Layout.fillWidth: true Layout.fillWidth: true
placeholderText: Translation.tr("Base URL") placeholderText: Translation.tr("Base URL")
text: Config.options.search.engineBaseUrl text: Config.options.search.engineBaseUrl
@@ -15,7 +15,10 @@ Rectangle {
color: Appearance.colors.colLayer1 color: Appearance.colors.colLayer1
property int selectedTab: 0 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) => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_PageDown || event.key === Qt.Key_PageUp) { 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
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "./quickToggles/"
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@@ -16,8 +15,6 @@ import Quickshell.Hyprland
Scope { Scope {
id: root id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 12
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
PanelWindow { PanelWindow {
id: sidebarRoot id: sidebarRoot
@@ -67,124 +64,7 @@ Scope {
} }
} }
sourceComponent: Item { sourceComponent: SidebarRightContent {}
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
}
}
}
}
} }
@@ -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
import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
@@ -10,13 +11,10 @@ import Quickshell.Hyprland
QuickToggleButton { QuickToggleButton {
id: root id: root
readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled toggled: BluetoothStatus.enabled
readonly property BluetoothDevice bluetoothDevice: Bluetooth.defaultAdapter.devices.values.find(device => device.connected) buttonIcon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
readonly property bool bluetoothConnected: bluetoothDevice !== undefined
toggled: bluetoothEnabled
buttonIcon: bluetoothConnected ? "bluetooth_connected" : bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
onClicked: { onClicked: {
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter.enabled Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
} }
altAction: () => { altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]) Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`])
@@ -24,7 +22,8 @@ QuickToggleButton {
} }
StyledToolTip { StyledToolTip {
content: Translation.tr("%1 | Right-click to configure").arg( content: Translation.tr("%1 | Right-click to configure").arg(
(bluetoothDevice?.name.length > 0) ? (BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
bluetoothDevice.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 color: rightSidebarButton.colText
} }
MaterialSymbol { MaterialSymbol {
readonly property bool bluetoothEnabled: Bluetooth.defaultAdapter.enabled text: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
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"
iconSize: Appearance.font.pixelSize.larger iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText 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 // Alcy doesn't provide dimensions and images are often of god resolution
"width": 1000, "width": 1000,
"height": 1000, "height": 1000,
"aspect_ratio": 1, // Default aspect ratio "aspect_ratio": 1,
"tags": "[no tags]", "tags": "[no tags]",
"rating": "s", "rating": "s",
"is_nsfw": false, "is_nsfw": false,
+2 -1
View File
@@ -1,3 +1,4 @@
import qs
import qs.modules.common import qs.modules.common
import QtQuick import QtQuick
import Quickshell import Quickshell
@@ -11,7 +12,7 @@ pragma ComponentBehavior: Bound
Singleton { Singleton {
property var clock: SystemClock { property var clock: SystemClock {
id: clock 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 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") property string shortDate: Qt.locale().toString(clock.date, Config.options?.time.shortDateFormat ?? "dd/MM")
@@ -13,10 +13,10 @@ import Quickshell.Io
Singleton { Singleton {
id: root id: root
property var manualActive property var manualActive
property string from: Config.options?.light?.night?.from ?? "19:00" // Default to 7 PM property string from: Config.options?.light?.night?.from ?? "19:00"
property string to: Config.options?.light?.night?.to ?? "06:30" // Default to 6:30 AM property string to: Config.options?.light?.night?.to ?? "06:30"
property bool automatic: Config.options?.light?.night?.automatic && (Config?.ready ?? true) 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 shouldBeOn
property bool firstEvaluation: true property bool firstEvaluation: true
property bool active: false property bool active: false
+174 -3
View File
@@ -1,12 +1,15 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
// Took many bits from https://github.com/caelestia-dots/shell (GPLv3)
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick import QtQuick
import "./network"
/** /**
* Simple polled network state service. * Network service with nmcli.
*/ */
Singleton { Singleton {
id: root id: root
@@ -15,6 +18,12 @@ Singleton {
property bool ethernet: false property bool ethernet: false
property bool wifiEnabled: 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 string networkName: ""
property int networkStrength property int networkStrength
property string materialSymbol: ethernet ? "lan" : property string materialSymbol: ethernet ? "lan" :
@@ -27,15 +36,103 @@ Singleton {
) : "signal_wifi_off" ) : "signal_wifi_off"
// Control // Control
function toggleWifi(): void { function enableWifi(enabled = true): void {
const cmd = wifiEnabled ? "off" : "on"; const cmd = enabled ? "on" : "off";
enableWifiProc.exec(["nmcli", "radio", "wifi", cmd]); 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 { Process {
id: enableWifiProc 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 // Status update
function update() { function update() {
updateConnectionType.startCheck(); 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/sidebarLeft/"
import "./modules/sidebarRight/" import "./modules/sidebarRight/"
import "./modules/verticalBar/" import "./modules/verticalBar/"
import "./modules/wallpaperSelector/"
import QtQuick import QtQuick
import QtQuick.Window import QtQuick.Window
@@ -49,6 +50,7 @@ ShellRoot {
property bool enableSidebarLeft: true property bool enableSidebarLeft: true
property bool enableSidebarRight: true property bool enableSidebarRight: true
property bool enableVerticalBar: true property bool enableVerticalBar: true
property bool enableWallpaperSelector: true
// Force initialization of some singletons // Force initialization of some singletons
Component.onCompleted: { Component.onCompleted: {
@@ -76,5 +78,6 @@ ShellRoot {
LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} } LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} }
LazyLoader { active: enableSidebarRight; component: SidebarRight {} } LazyLoader { active: enableSidebarRight; component: SidebarRight {} }
LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} } 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": "操作", "Action": "操作",
"Add": "追加", "Add": "追加",
"Add task": "タスクを追加", "Add task": "タスクを追加",
"All-rounder | Good quality, decent quantity": "万能型 | 品質良好、量も十分", "All-rounder | Good quality, decent quantity": "万能型 | 品質・十分な量",
"Allow NSFW": "NSFWを許可", "Allow NSFW": "NSFWを許可",
"Allow NSFW content": "NSFWコンテンツを許可", "Allow NSFW content": "NSFWコンテンツを許可",
"Anime": "アニメ", "Anime": "アニメ",
"Anime boorus": "アニメ画像掲示板", "Anime boorus": "アニメ画像掲示板",
"App": "アプリ", "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", "Bluetooth": "Bluetooth",
"Brightness": "明るさ", "Brightness": "明るさ",
"Cancel": "キャンセル", "Cancel": "キャンセル",
"Chain of Thought": "思考プロセス",
"Cheat sheet": "チートシート", "Cheat sheet": "チートシート",
"Choose model": "モデルを選択", "Choose model": "モデルを選択",
"Clean stuff | Excellent quality, no NSFW": "健全 | 高品質NSFWなし", "Clean stuff | Excellent quality, no NSFW": "健全 | 高品質NSFWなし",
"Clear": "クリア", "Clear": "クリア",
"Clear chat history": "チャット履歴を消去", "Clear chat history": "チャット履歴を消去",
"Clear the current list of images": "現在の画像リストをクリア", "Clear the current list of images": "現在の画像リストをクリア",
@@ -42,7 +41,7 @@
"Edit": "編集", "Edit": "編集",
"Enter text to translate...": "翻訳するテキストを入力...", "Enter text to translate...": "翻訳するテキストを入力...",
"Finished tasks will go here": "完了したタスクはここに表示されます", "Finished tasks will go here": "完了したタスクはここに表示されます",
"For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 質", "For desktop wallpapers | Good quality": "デスクトップ壁紙向け | 高品質",
"For storing API keys and other sensitive information": "APIキーや機密情報の保存用", "For storing API keys and other sensitive information": "APIキーや機密情報の保存用",
"Game mode": "ゲームモード", "Game mode": "ゲームモード",
"Get the next page of results": "次のページを取得", "Get the next page of results": "次のページを取得",
@@ -53,14 +52,13 @@
"Invalid arguments. Must provide `key` and `value`.": "無効な引数です。`key`と`value`を指定してください。", "Invalid arguments. Must provide `key` and `value`.": "無効な引数です。`key`と`value`を指定してください。",
"Jump to current month": "現在の月へ移動", "Jump to current month": "現在の月へ移動",
"Keep system awake": "システムをスリープさせない", "Keep system awake": "システムをスリープさせない",
"Large images | God tier quality, no NSFW.": "大きな画像 | 最高品質NSFWなし", "Large images | God tier quality, no NSFW.": "大きな画像 | 最高品質NSFWなし",
"Large language models": "大規模言語モデル", "Large language models": "大規模言語モデル",
"Launch": "起動", "Launch": "起動",
"Lock": "ロック", "Lock": "ロック",
"Logout": "ログアウト", "Logout": "ログアウト",
"Markdown test": "Markdownテスト", "Markdown test": "Markdownテスト",
"Math result": "計算結果", "Math result": "計算結果",
"Night Light": "夜間モード",
"No audio source": "音声ソースなし", "No audio source": "音声ソースなし",
"No media": "メディアなし", "No media": "メディアなし",
"No notifications": "通知なし", "No notifications": "通知なし",
@@ -83,7 +81,7 @@
"Select Language": "言語を選択", "Select Language": "言語を選択",
"Session": "セッション", "Session": "セッション",
"Set API key": "APIキーを設定", "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プロバイダーを設定します", "Set the current API provider": "現在のAPIプロバイダーを設定します",
"Shutdown": "シャットダウン", "Shutdown": "シャットダウン",
"Silent": "サイレント", "Silent": "サイレント",
@@ -92,8 +90,8 @@
"Task Manager": "タスクマネージャー", "Task Manager": "タスクマネージャー",
"Task description": "タスクの説明", "Task description": "タスクの説明",
"Temperature must be between 0 and 2": "温度は0~2の間で指定してください", "Temperature must be between 0 and 2": "温度は0~2の間で指定してください",
"The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "エロ系 | 量が多くNSFW多数、品質はバラバラ", "The hentai one | Great quantity, a lot of NSFW, quality varies wildly": "成人向け | 量は多いが品質は様々・NSFW多数",
"The popular one | Best quantity, but quality can vary wildly": "人気 | 量は最多品質はバラつきあり", "The popular one | Best quantity, but quality can vary wildly": "人気 | 量は最多だが品質は様々",
"Thinking": "考え中", "Thinking": "考え中",
"Translation goes here...": "ここに翻訳が表示されます...", "Translation goes here...": "ここに翻訳が表示されます...",
"Translator": "翻訳", "Translator": "翻訳",
@@ -104,21 +102,17 @@
"Unknown Title": "不明なタイトル", "Unknown Title": "不明なタイトル",
"View Markdown source": "Markdownソースを表示", "View Markdown source": "Markdownソースを表示",
"Volume": "音量", "Volume": "音量",
"Volume mixer": "音量ミキサー", "Volume mixer": "ボリュームミキサー",
"Waifus only | Excellent quality, limited quantity": "美少女系のみ | 高品質量は少なめ", "Waifus only | Excellent quality, limited quantity": "キャラクター | 高品質量は少なめ",
"Waiting for response...": "応答待ち...", "Waiting for response...": "応答待ち...",
"Workspace": "ワークスペース", "Workspace": "ワークスペース",
"Set with /mode PROVIDER": "/mode PROVIDER で設定",
"Invalid API provider. Supported: \n-": "無効なAPIプロバイダー。対応: \n-", "Invalid API provider. Supported: \n-": "無効なAPIプロバイダー。対応: \n-",
"Unknown command:": "不明なコマンド:", "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でサイドバーをウィンドウ化", "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のエンドポイント:",
"Provider set to": "プロバイダーを設定しました:", "Provider set to": "プロバイダーを設定しました:",
"Invalid model. Supported: \n```": "無効なモデル。対応: \n```", "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- タグがなければページ番号を入力", "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.": "検索モードに切り替えました。リクエストを続行します。", "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": "設定", "Settings": "設定",
"Save chat": "チャットを保存", "Save chat": "チャットを保存",
"Load chat": "チャットを読み込み", "Load chat": "チャットを読み込み",
@@ -137,12 +131,12 @@
"Material palette": "マテリアルパレット", "Material palette": "マテリアルパレット",
"Fidelity": "忠実度", "Fidelity": "忠実度",
"Fruit Salad": "フルーツサラダ", "Fruit Salad": "フルーツサラダ",
"Alternatively use /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgも利用可能", "Alternatively use /dark, /light, /img in the launcher": "ランチャーで /dark, /light, /img も使用できます",
"Fake screen rounding": "画面の角を疑似的に丸める", "Fake screen rounding": "画面の角を丸める(疑似)",
"When not fullscreen": "全画面でない場合", "When not fullscreen": "フルスクリーンでない",
"Choose file": "ファイルを選択", "Choose file": "ファイルを選択",
"Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "KonachanからランダムなSFW健全アニメ壁紙\n画像は~/Pictures/Wallpapersに保存されます", "Random SFW Anime wallpaper from Konachan\nImage is saved to ~/Pictures/Wallpapers": "Konachanから健全アニメ壁紙をランダムで取得し、~/Pictures/Wallpapers に保存ます",
"Be patient...": "しばらくお待ちください...", "Be patient...": "少々お待ちください",
"Decorations & Effects": "装飾と効果", "Decorations & Effects": "装飾と効果",
"Tonal Spot": "トーナルスポット", "Tonal Spot": "トーナルスポット",
"Shell windows": "シェルウィンドウ", "Shell windows": "シェルウィンドウ",
@@ -152,25 +146,24 @@
"Title bar": "タイトルバー", "Title bar": "タイトルバー",
"Transparency": "透明度", "Transparency": "透明度",
"Expressive": "表現豊か", "Expressive": "表現豊か",
"Yes": "はい", "Yes": "表示",
"Enable": "有効化", "Enable": "有効化",
"Rainbow": "レインボー", "Rainbow": "レインボー",
"Might look ass. Unsupported.": "見た目が悪くなる場合があります(未サポート", "Might look ass. Unsupported.": "表示が崩れる可能性があります(非推奨",
"Monochrome": "モノクロ", "Monochrome": "モノクロ",
"Random: Konachan": "ランダム: Konachan", "Random: Konachan": "ランダム: Konachan",
"Center title": "タイトルを中央に", "Center title": "タイトルを中央に",
"Neutral": "ニュートラル", "Neutral": "ニュートラル",
"Pick wallpaper image on your system": "システムから壁紙画像を選択", "Pick wallpaper image on your system": "システムから壁紙画像を選択",
"No": "いいえ", "No": "非表示",
"AI": "AI", "AI": "AI",
"Local only": "ローカルのみ", "Local only": "ローカルのみ",
"Policies": "ポリシー", "Policies": "ポリシー",
"Weeb": "オタク向け", "Weeb": "アニメファン向け",
"Closet": "クローゼット", "Closet": "隠し",
"Bar style": "バーのスタイル",
"Show next time": "次回も表示する", "Show next time": "次回も表示する",
"Usage": "使用状況", "Usage": "使用状況",
"Plain rectangle": "ただの四角形", "Plain rectangle": "長方形",
"Useless buttons": "ダミーボタン", "Useless buttons": "ダミーボタン",
"GitHub": "GitHub", "GitHub": "GitHub",
"Style & wallpaper": "スタイルと壁紙", "Style & wallpaper": "スタイルと壁紙",
@@ -178,12 +171,11 @@
"Change any time later with /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgでいつでも変更可能", "Change any time later with /dark, /light, /img in the launcher": "ランチャーで/dark, /light, /imgでいつでも変更可能",
"Keybinds": "キー割り当て", "Keybinds": "キー割り当て",
"Float": "フローティング", "Float": "フローティング",
"Hug": "ハグ", "Hug": "固定",
"Yooooo hi there": "やあ、こんにちは", "illogical-impulse Welcome": "illogical-impulse へようこそ",
"illogical-impulse Welcome": "illogical-impulseへようこそ",
"Info": "情報", "Info": "情報",
"Volume limit": "音量制限", "Volume limit": "ボリューム制限",
"Prevents abrupt increments and restricts volume limit": "急な音量上昇を防ぎ、音量を制限します", "Prevents abrupt increments and restricts volume limit": "急な音量変化を防ぎ、音量の上限を設定します",
"Resources": "リソース", "Resources": "リソース",
"12h am/pm": "12時間(AM/PM", "12h am/pm": "12時間(AM/PM",
"Base URL": "ベースURL", "Base URL": "ベースURL",
@@ -194,32 +186,31 @@
"Battery": "バッテリー", "Battery": "バッテリー",
"Prefixes": "接頭辞", "Prefixes": "接頭辞",
"Emojis": "絵文字", "Emojis": "絵文字",
"Earbang protection": "急音防止", "Earbang protection": "聴覚保護(急な大音量防止",
"Automatically suspends the system when battery is low": "バッテリー残量が少ないときに自動でスリープします", "Automatically suspends the system when battery is low": "バッテリー残量が少ないときに自動でスリープします",
"Automatic suspend": "自動スリープ", "Automatic suspend": "自動スリープ",
"Suspend at": "スリープ開始残量(%", "Suspend at": "スリープ開始残量(%",
"Max allowed increase": "最大許容増加幅", "Max allowed increase": "音量の最大増加幅",
"Web search": "ウェブ検索", "Web search": "ウェブ検索",
"Polling interval (ms)": "ポーリング間隔(ms", "Polling interval (ms)": "ポーリング間隔(ms",
"Clipboard": "クリップボード", "Clipboard": "クリップボード",
"Low warning": "バッテリー低下の警告", "Low warning": "バッテリー低下の警告",
"24h": "24時間", "24h": "24時間",
"Use Levenshtein distance-based algorithm instead of fuzzy": "ファジー検索の代わりにレーベンシュタイン距離ベースのアルゴリズムを使用", "Use Levenshtein distance-based algorithm instead of fuzzy": "あいまい検索の代わりにレーベンシュタイン距離アルゴリズムを使用",
"System prompt": "システムプロンプト", "System prompt": "システムプロンプト",
"12h AM/PM": "12時間(AM/PM", "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": "重大な警告", "Critical warning": "重大な警告",
"User agent (for services that require it)": "ユーザーエージェント(必要なサービス用)", "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は使用していません。", "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": "オフにすると可読性が下がる場合があります", "Note: turning off can hurt readability": "注意: オフにすると可読性が損なわれる場合があります",
"Workspaces shown": "表示中のワークスペース", "Workspaces shown": "表示中のワークスペース",
"Dark/Light toggle": "ダーク/ライト切替", "Dark/Light toggle": "ダーク/ライト切替",
"Dock": "ドック", "Dock": "ドック",
"Weather": "天気", "Weather": "天気",
"Pinned on startup": "起動時にピン留め", "Pinned on startup": "起動時にピン留め",
"Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント: アイコンを隠して常に数字を表示すると、クラシックなillogical-impulse体験になります", "Tip: Hide icons and always show numbers for\nthe classic illogical-impulse experience": "ヒント: アイコンを非表示にして番号を常に表示すると、クラシックなillogical-impulseの使用感を体験できます",
"Appearance": "外観", "Always show numbers": "数字を常に表示",
"Always show numbers": "常に数字を表示",
"Buttons": "ボタン", "Buttons": "ボタン",
"Keyboard toggle": "キーボード切替", "Keyboard toggle": "キーボード切替",
"Scale (%)": "スケール(%)", "Scale (%)": "スケール(%)",
@@ -232,8 +223,8 @@
"Show app icons": "アプリアイコンを表示", "Show app icons": "アプリアイコンを表示",
"Workspaces": "ワークスペース", "Workspaces": "ワークスペース",
"Columns": "列数", "Columns": "列数",
"On-screen display": "オンスクリーン表示", "On-screen display": "画面表示",
"Screen snip": "画面切り取り", "Screen snip": "画面切り抜き",
"Mic toggle": "マイク切替", "Mic toggle": "マイク切替",
"Hover to reveal": "ホバーで表示", "Hover to reveal": "ホバーで表示",
"Bar": "バー", "Bar": "バー",
@@ -247,7 +238,7 @@
"Distro": "ディストリビューション", "Distro": "ディストリビューション",
"Privacy Policy": "プライバシーポリシー", "Privacy Policy": "プライバシーポリシー",
"Documentation": "ドキュメント", "Documentation": "ドキュメント",
"Shell & utilities theming must also be enabled": "シェルとユーティリティのテーマも有効にする必要があります", "Shell & utilities theming must also be enabled": "シェルとユーティリティのテーマ設定も有効にする必要があります",
"illogical-impulse": "illogical-impulse", "illogical-impulse": "illogical-impulse",
"Donate": "寄付", "Donate": "寄付",
"Terminal": "ターミナル", "Terminal": "ターミナル",
@@ -257,7 +248,7 @@
"Issues": "課題", "Issues": "課題",
"Drag or click a region • LMB: Copy • RMB: Edit": "領域をドラッグまたはクリック • 左クリック: コピー • 右クリック: 編集", "Drag or click a region • LMB: Copy • RMB: Edit": "領域をドラッグまたはクリック • 左クリック: コピー • 右クリック: 編集",
"Current model: %1\nSet it with %2model MODEL": "現在のモデル: %1\n%2model MODELで設定", "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キーが設定されていません", "No API key set for %1": "%1のAPIキーが設定されていません",
"Loaded the following system prompt\n\n---\n\n%1": "次のシステムプロンプトを読み込みました\n\n---\n\n%1", "Loaded the following system prompt\n\n---\n\n%1": "次のシステムプロンプトを読み込みました\n\n---\n\n%1",
"%1 | Right-click to configure": "%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で設定", "Current API endpoint: %1\nSet it with %2mode PROVIDER": "現在のAPIエンドポイント: %1\n%2mode PROVIDERで設定",
"Go to source (%1)": "ソースを開く(%1", "Go to source (%1)": "ソースを開く(%1",
"Temperature set to %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」", "Enter tags, or \"%1\" for commands": "タグを入力、またはコマンドは「%1」",
"%1 queries pending": "%1件のクエリが保留中", "%1 queries pending": "%1件のクエリが保留中",
"API key:\n\n```txt\n%1\n```": "APIキー:\n\n```txt\n%1\n```", "API key:\n\n```txt\n%1\n```": "APIキー:\n\n```txt\n%1\n```",
"Uptime: %1": "稼働時間: %1",
"%1 Safe Storage": "%1 セーフストレージ", "%1 Safe Storage": "%1 セーフストレージ",
"%1 does not require an API key": "%1はAPIキー不要です", "%1 does not require an API key": "%1はAPIキー不要です",
"Temperature: %1": "温度: %1", "Temperature: %1": "温度: %1",
@@ -288,19 +277,19 @@
"Critically low battery": "バッテリー残量が非常に少ない", "Critically low battery": "バッテリー残量が非常に少ない",
"Select output device": "出力デバイスを選択", "Select output device": "出力デバイスを選択",
"Code saved to file": "コードをファイルに保存しました", "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": "スクロールで音量調整", "Scroll to change volume": "スクロールで音量調整",
"Elements": "要素", "Elements": "要素",
"%1 • %2 tasks": "%1 • %2件のタスク", "%1 • %2 tasks": "%1 • %2件のタスク",
"Download complete": "ダウンロード完了", "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": "Cloudflare WARP",
"Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)", "Cloudflare WARP (1.1.1.1)": "Cloudflare WARP (1.1.1.1)",
"Scroll to change brightness": "スクロールで明るさ調整", "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": "入力デバイスを選択", "Select input device": "入力デバイスを選択",
"Registration failed. Please inspect manually with the <tt>warp-cli</tt> command": "登録に失敗しました。<tt>warp-cli</tt>コマンドで手動確認してください", "Registration failed. Please inspect manually with the <tt>warp-cli</tt> command": "登録に失敗しました。<tt>warp-cli</tt> コマンドで手動確認してください",
"Consider plugging in your device": "電源接続してください", "Consider plugging in your device": "電源接続することを推奨します",
"Low battery": "バッテリー残量低下", "Low battery": "バッテリー残量低下",
"Saved to %1": "%1に保存しました", "Saved to %1": "%1に保存しました",
"Sunset": "日没", "Sunset": "日没",
@@ -316,23 +305,128 @@
"Fully charged": "充電完了", "Fully charged": "充電完了",
"Charging:": "充電中:", "Charging:": "充電中:",
"Discharging:": "放電中:", "Discharging:": "放電中:",
"Uptime:": "稼働時間:",
"Upcoming Tasks:": "今後のタスク:",
"No pending tasks": "保留中のタスクなし", "No pending tasks": "保留中のタスクなし",
"... and %1 more": "...他%1件", "... and %1 more": "...他%1件",
"Memory Usage": "メモリ使用量",
"Used:": "使用済み:", "Used:": "使用済み:",
"Free:": "空き:", "Free:": "空き:",
"Total:": "合計:", "Total:": "合計:",
"Usage:": "使用状況:",
"Swap Usage": "スワップ使用量",
"Swap:": "スワップ:", "Swap:": "スワップ:",
"Not configured": "未設定", "Not configured": "未設定",
"CPU Usage": "CPU使用率",
"Current:": "現在:",
"Load:": "負荷:", "Load:": "負荷:",
"High": "高", "High": "高",
"Medium": "中", "Medium": "中",
"Low": "低", "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 libsass
material-color-utilities material-color-utilities
setproctitle setproctitle
click
loguru
pycairo
pygobject
tqdm
+12
View File
@@ -4,8 +4,12 @@ build==1.2.2.post1
# via -r scriptdata/requirements.in # via -r scriptdata/requirements.in
cffi==1.17.1 cffi==1.17.1
# via pywayland # via pywayland
click==8.2.1
# via -r scriptdata/requirements.in
libsass==0.23.0 libsass==0.23.0
# via -r scriptdata/requirements.in # via -r scriptdata/requirements.in
loguru==0.7.3
# via -r scriptdata/requirements.in
material-color-utilities==0.2.1 material-color-utilities==0.2.1
# via -r scriptdata/requirements.in # via -r scriptdata/requirements.in
materialyoucolor==2.0.10 materialyoucolor==2.0.10
@@ -22,8 +26,14 @@ pillow==11.1.0
# material-color-utilities # material-color-utilities
psutil==6.1.1 psutil==6.1.1
# via -r scriptdata/requirements.in # via -r scriptdata/requirements.in
pycairo==1.28.0
# via
# -r scriptdata/requirements.in
# pygobject
pycparser==2.22 pycparser==2.22
# via cffi # via cffi
pygobject==3.52.3
# via -r scriptdata/requirements.in
pyproject-hooks==1.2.0 pyproject-hooks==1.2.0
# via build # via build
pywayland==0.4.18 pywayland==0.4.18
@@ -34,5 +44,7 @@ setuptools==80.9.0
# via setuptools-scm # via setuptools-scm
setuptools-scm==8.1.0 setuptools-scm==8.1.0
# via -r scriptdata/requirements.in # via -r scriptdata/requirements.in
tqdm==4.67.1
# via -r scriptdata/requirements.in
wheel==0.45.1 wheel==0.45.1
# via -r scriptdata/requirements.in # via -r scriptdata/requirements.in