publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed from the internal source named below under a manifest that decides which paths may leave, then scanned as a whole tree rather than as a series of patches, and only then published. Public history starts here because the history before it was not admissible, and neither was the tree. What used to stand in this repository included a rescue copy of another machine, a directory of phone handoffs, deployment wired to one house, and a submodule pointing at a forge no stranger can reach. None of that was ever the product. It stays in the private forge, which is allowed to hold the whole working organism, and this is what was deliberately sent out instead. Three mechanisms produced this tree, in decreasing order of trust. A top-level path the manifest does not name never arrives at all, which is the one that catches directories nobody has thought of yet. Named internal files inside admitted roots are dropped. A short, reviewed table replaces deployment defaults that a public build must not carry -- an endpoint aimed at one LAN, a VPN profile belonging to one phone, packaging built from one checkout path. Everything after this commit is an ordinary publication with the same three trailers, so a force push stops being routine and starts meaning that something deliberate happened. The trailers bind the projection to its source without pretending the public SHA is the private one: same lineage, different tree, and the record says so. Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047 Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27 Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
commit
8f42fc953d
1476 changed files with 238455 additions and 0 deletions
418
surfaces/quickshell/ii-base/modules/common/Appearance.qml
Normal file
418
surfaces/quickshell/ii-base/modules/common/Appearance.qml
Normal file
|
|
@ -0,0 +1,418 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common.functions
|
||||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property QtObject m3colors
|
||||
property QtObject animation
|
||||
property QtObject animationCurves
|
||||
property QtObject colors
|
||||
property QtObject rounding
|
||||
property QtObject font
|
||||
property QtObject sizes
|
||||
property string syntaxHighlightingTheme
|
||||
|
||||
// Transparency. The quadratic functions were derived from analysis of hand-picked transparency values.
|
||||
ColorQuantizer {
|
||||
id: wallColorQuant
|
||||
property string wallpaperPath: Config.options.background.wallpaperPath
|
||||
property bool wallpaperIsVideo: wallpaperPath.endsWith(".mp4") || wallpaperPath.endsWith(".webm") || wallpaperPath.endsWith(".mkv") || wallpaperPath.endsWith(".avi") || wallpaperPath.endsWith(".mov")
|
||||
source: Qt.resolvedUrl(wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath)
|
||||
depth: 0 // 2^0 = 1 color
|
||||
rescaleSize: 10
|
||||
}
|
||||
property real wallpaperVibrancy: (wallColorQuant.colors[0]?.hslSaturation + wallColorQuant.colors[0]?.hslLightness) / 2
|
||||
property real autoBackgroundTransparency: { // y = 0.5768x^2 - 0.759x + 0.2896
|
||||
let x = wallpaperVibrancy
|
||||
let y = 0.5768 * (x * x) - 0.759 * (x) + 0.2896
|
||||
return Math.max(0, Math.min(0.22, y)) - 0.12 * (m3colors.darkmode ? 0 : 1)
|
||||
}
|
||||
property real autoContentTransparency: 0.9
|
||||
property real backgroundTransparency: Config?.options.appearance.transparency.enable ? Config?.options.appearance.transparency.automatic ? autoBackgroundTransparency : Config?.options.appearance.transparency.backgroundTransparency : 0
|
||||
property real contentTransparency: Config?.options.appearance.transparency.automatic ? autoContentTransparency : Config?.options.appearance.transparency.contentTransparency
|
||||
|
||||
m3colors: QtObject {
|
||||
property bool darkmode: true
|
||||
property bool transparent: false
|
||||
property color m3background: "#141313"
|
||||
property color m3onBackground: "#e6e1e1"
|
||||
property color m3surface: "#141313"
|
||||
property color m3surfaceDim: "#141313"
|
||||
property color m3surfaceBright: "#3a3939"
|
||||
property color m3surfaceContainerLowest: "#0f0e0e"
|
||||
property color m3surfaceContainerLow: "#1c1b1c"
|
||||
property color m3surfaceContainer: "#201f20"
|
||||
property color m3surfaceContainerHigh: "#2b2a2a"
|
||||
property color m3surfaceContainerHighest: "#363435"
|
||||
property color m3onSurface: "#e6e1e1"
|
||||
property color m3surfaceVariant: "#49464a"
|
||||
property color m3onSurfaceVariant: "#cbc5ca"
|
||||
property color m3inverseSurface: "#e6e1e1"
|
||||
property color m3inverseOnSurface: "#313030"
|
||||
property color m3outline: "#948f94"
|
||||
property color m3outlineVariant: "#49464a"
|
||||
property color m3shadow: "#000000"
|
||||
property color m3scrim: "#000000"
|
||||
property color m3surfaceTint: "#cbc4cb"
|
||||
property color m3primary: "#cbc4cb"
|
||||
property color m3onPrimary: "#322f34"
|
||||
property color m3primaryContainer: "#2d2a2f"
|
||||
property color m3onPrimaryContainer: "#bcb6bc"
|
||||
property color m3inversePrimary: "#615d63"
|
||||
property color m3secondary: "#cac5c8"
|
||||
property color m3onSecondary: "#323032"
|
||||
property color m3secondaryContainer: "#4d4b4d"
|
||||
property color m3onSecondaryContainer: "#ece6e9"
|
||||
property color m3tertiary: "#d1c3c6"
|
||||
property color m3onTertiary: "#372e30"
|
||||
property color m3tertiaryContainer: "#31292b"
|
||||
property color m3onTertiaryContainer: "#c1b4b7"
|
||||
property color m3error: "#ffb4ab"
|
||||
property color m3onError: "#690005"
|
||||
property color m3errorContainer: "#93000a"
|
||||
property color m3onErrorContainer: "#ffdad6"
|
||||
property color m3primaryFixed: "#e7e0e7"
|
||||
property color m3primaryFixedDim: "#cbc4cb"
|
||||
property color m3onPrimaryFixed: "#1d1b1f"
|
||||
property color m3onPrimaryFixedVariant: "#49454b"
|
||||
property color m3secondaryFixed: "#e6e1e4"
|
||||
property color m3secondaryFixedDim: "#cac5c8"
|
||||
property color m3onSecondaryFixed: "#1d1b1d"
|
||||
property color m3onSecondaryFixedVariant: "#484648"
|
||||
property color m3tertiaryFixed: "#eddfe1"
|
||||
property color m3tertiaryFixedDim: "#d1c3c6"
|
||||
property color m3onTertiaryFixed: "#211a1c"
|
||||
property color m3onTertiaryFixedVariant: "#4e4447"
|
||||
property color m3success: "#B5CCBA"
|
||||
property color m3onSuccess: "#213528"
|
||||
property color m3successContainer: "#374B3E"
|
||||
property color m3onSuccessContainer: "#D1E9D6"
|
||||
property color term0: "#EDE4E4"
|
||||
property color term1: "#B52755"
|
||||
property color term2: "#A97363"
|
||||
property color term3: "#AF535D"
|
||||
property color term4: "#A67F7C"
|
||||
property color term5: "#B2416B"
|
||||
property color term6: "#8D76AD"
|
||||
property color term7: "#272022"
|
||||
property color term8: "#0E0D0D"
|
||||
property color term9: "#B52755"
|
||||
property color term10: "#A97363"
|
||||
property color term11: "#AF535D"
|
||||
property color term12: "#A67F7C"
|
||||
property color term13: "#B2416B"
|
||||
property color term14: "#8D76AD"
|
||||
property color term15: "#221A1A"
|
||||
}
|
||||
|
||||
colors: QtObject {
|
||||
property color colSubtext: m3colors.m3outline
|
||||
// Layer 0
|
||||
property color colLayer0Base: ColorUtils.mix(m3colors.m3background, m3colors.m3primary, Config.options.appearance.extraBackgroundTint ? 0.99 : 1)
|
||||
property color colLayer0: ColorUtils.transparentize(colLayer0Base, root.backgroundTransparency)
|
||||
property color colOnLayer0: m3colors.m3onBackground
|
||||
property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency))
|
||||
property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency))
|
||||
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
|
||||
// Layer 1
|
||||
property color colLayer1Base: m3colors.m3surfaceContainerLow
|
||||
property color colLayer1: ColorUtils.solveOverlayColor(colLayer0Base, colLayer1Base, 1 - root.contentTransparency);
|
||||
property color colOnLayer1: m3colors.m3onSurfaceVariant;
|
||||
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45);
|
||||
property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency)
|
||||
property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency);
|
||||
// Layer 2
|
||||
property color colLayer2Base: m3colors.m3surfaceContainer
|
||||
property color colLayer2: ColorUtils.solveOverlayColor(colLayer1Base, colLayer2Base, 1 - root.contentTransparency)
|
||||
property color colLayer2Hover: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.90), 1 - root.contentTransparency)
|
||||
property color colLayer2Active: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, colOnLayer2, 0.80), 1 - root.contentTransparency);
|
||||
property color colLayer2Disabled: ColorUtils.solveOverlayColor(colLayer1Base, ColorUtils.mix(colLayer2Base, m3colors.m3background, 0.8), 1 - root.contentTransparency);
|
||||
property color colOnLayer2: m3colors.m3onSurface;
|
||||
property color colOnLayer2Disabled: ColorUtils.mix(colOnLayer2, m3colors.m3background, 0.4);
|
||||
// Layer 3
|
||||
property color colLayer3Base: m3colors.m3surfaceContainerHigh
|
||||
property color colLayer3: ColorUtils.solveOverlayColor(colLayer2Base, colLayer3Base, 1 - root.contentTransparency)
|
||||
property color colLayer3Hover: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.90), 1 - root.contentTransparency)
|
||||
property color colLayer3Active: ColorUtils.solveOverlayColor(colLayer2Base, ColorUtils.mix(colLayer3Base, colOnLayer3, 0.80), 1 - root.contentTransparency);
|
||||
property color colOnLayer3: m3colors.m3onSurface;
|
||||
// Layer 4
|
||||
property color colLayer4Base: m3colors.m3surfaceContainerHighest
|
||||
property color colLayer4: ColorUtils.solveOverlayColor(colLayer3Base, colLayer4Base, 1 - root.contentTransparency)
|
||||
property color colLayer4Hover: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.90), 1 - root.contentTransparency)
|
||||
property color colLayer4Active: ColorUtils.solveOverlayColor(colLayer3Base, ColorUtils.mix(colLayer4Base, colOnLayer4, 0.80), 1 - root.contentTransparency);
|
||||
property color colOnLayer4: m3colors.m3onSurface;
|
||||
// Primary
|
||||
property color colPrimary: m3colors.m3primary
|
||||
property color colOnPrimary: m3colors.m3onPrimary
|
||||
property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87)
|
||||
property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7)
|
||||
property color colPrimaryContainer: m3colors.m3primaryContainer
|
||||
property color colPrimaryContainerHover: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.9)
|
||||
property color colPrimaryContainerActive: ColorUtils.mix(colors.colPrimaryContainer, colors.colOnPrimaryContainer, 0.8)
|
||||
property color colOnPrimaryContainer: m3colors.m3onPrimaryContainer
|
||||
// Secondary
|
||||
property color colSecondary: m3colors.m3secondary
|
||||
property color colSecondaryHover: ColorUtils.mix(m3colors.m3secondary, colLayer1Hover, 0.85)
|
||||
property color colSecondaryActive: ColorUtils.mix(m3colors.m3secondary, colLayer1Active, 0.4)
|
||||
property color colOnSecondary: m3colors.m3onSecondary
|
||||
property color colSecondaryContainer: m3colors.m3secondaryContainer
|
||||
property color colSecondaryContainerHover: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.90)
|
||||
property color colSecondaryContainerActive: ColorUtils.mix(m3colors.m3secondaryContainer, m3colors.m3onSecondaryContainer, 0.54)
|
||||
property color colOnSecondaryContainer: m3colors.m3onSecondaryContainer
|
||||
// Tertiary
|
||||
property color colTertiary: m3colors.m3tertiary
|
||||
property color colTertiaryHover: ColorUtils.mix(m3colors.m3tertiary, colLayer1Hover, 0.85)
|
||||
property color colTertiaryActive: ColorUtils.mix(m3colors.m3tertiary, colLayer1Active, 0.4)
|
||||
property color colTertiaryContainer: m3colors.m3tertiaryContainer
|
||||
property color colTertiaryContainerHover: ColorUtils.mix(m3colors.m3tertiaryContainer, m3colors.m3onTertiaryContainer, 0.90)
|
||||
property color colTertiaryContainerActive: ColorUtils.mix(m3colors.m3tertiaryContainer, colLayer1Active, 0.54)
|
||||
property color colOnTertiary: m3colors.m3onTertiary
|
||||
property color colOnTertiaryContainer: m3colors.m3onTertiaryContainer
|
||||
// Surface
|
||||
property color colBackgroundSurfaceContainer: ColorUtils.transparentize(m3colors.m3surfaceContainer, root.backgroundTransparency)
|
||||
property color colSurfaceContainerLow: ColorUtils.solveOverlayColor(m3colors.m3background, m3colors.m3surfaceContainerLow, 1 - root.contentTransparency)
|
||||
property color colSurfaceContainer: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerLow, m3colors.m3surfaceContainer, 1 - root.contentTransparency)
|
||||
property color colSurfaceContainerHigh: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainer, m3colors.m3surfaceContainerHigh, 1 - root.contentTransparency)
|
||||
property color colSurfaceContainerHighest: ColorUtils.solveOverlayColor(m3colors.m3surfaceContainerHigh, m3colors.m3surfaceContainerHighest, 1 - root.contentTransparency)
|
||||
property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
|
||||
property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85)
|
||||
property color colOnSurface: m3colors.m3onSurface
|
||||
property color colOnSurfaceVariant: m3colors.m3onSurfaceVariant
|
||||
// Misc
|
||||
property color colTooltip: m3colors.m3inverseSurface
|
||||
property color colOnTooltip: m3colors.m3inverseOnSurface
|
||||
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
|
||||
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
|
||||
property color colOutline: m3colors.m3outline
|
||||
property color colOutlineVariant: m3colors.m3outlineVariant
|
||||
property color colError: m3colors.m3error
|
||||
property color colErrorHover: ColorUtils.mix(m3colors.m3error, colLayer1Hover, 0.85)
|
||||
property color colErrorActive: ColorUtils.mix(m3colors.m3error, colLayer1Active, 0.7)
|
||||
property color colOnError: m3colors.m3onError
|
||||
property color colErrorContainer: m3colors.m3errorContainer
|
||||
property color colErrorContainerHover: ColorUtils.mix(m3colors.m3errorContainer, m3colors.m3onErrorContainer, 0.90)
|
||||
property color colErrorContainerActive: ColorUtils.mix(m3colors.m3errorContainer, m3colors.m3onErrorContainer, 0.70)
|
||||
property color colOnErrorContainer: m3colors.m3onErrorContainer
|
||||
}
|
||||
|
||||
rounding: QtObject {
|
||||
property int unsharpen: 2
|
||||
property int unsharpenmore: 6
|
||||
property int verysmall: 8
|
||||
property int small: 12
|
||||
property int normal: 17
|
||||
property int large: 23
|
||||
property int verylarge: 30
|
||||
property int full: 9999
|
||||
property int screenRounding: large
|
||||
property int windowRounding: 18
|
||||
}
|
||||
|
||||
font: QtObject {
|
||||
property QtObject family: QtObject {
|
||||
property string main: Config.options.appearance.fonts.main
|
||||
property string numbers: Config.options.appearance.fonts.numbers
|
||||
property string title: Config.options.appearance.fonts.title
|
||||
property string iconMaterial: "Material Symbols Rounded"
|
||||
property string iconNerd: Config.options.appearance.fonts.iconNerd
|
||||
property string monospace: Config.options.appearance.fonts.monospace
|
||||
property string reading: Config.options.appearance.fonts.reading
|
||||
property string expressive: Config.options.appearance.fonts.expressive
|
||||
}
|
||||
property QtObject variableAxes: QtObject {
|
||||
property var main: ({
|
||||
"wght": 450,
|
||||
"wdth": 100,
|
||||
})
|
||||
property var numbers: ({
|
||||
"wght": 450,
|
||||
})
|
||||
property var title: ({ // Slightly bold weight for title
|
||||
"wght": 550, // Weight (Lowered to compensate for increased grade)
|
||||
})
|
||||
}
|
||||
property QtObject pixelSize: QtObject {
|
||||
property int smallest: 10
|
||||
property int smaller: 12
|
||||
property int smallie: 13
|
||||
property int small: 15
|
||||
property int normal: 16
|
||||
property int large: 17
|
||||
property int larger: 19
|
||||
property int huge: 22
|
||||
property int hugeass: 23
|
||||
property int title: huge
|
||||
}
|
||||
}
|
||||
|
||||
animationCurves: QtObject {
|
||||
readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms
|
||||
readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms
|
||||
readonly property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms
|
||||
readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms
|
||||
readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedFirstHalf: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82]
|
||||
readonly property list<real> emphasizedLastHalf: [5 / 24, 0.82, 0.25, 1, 1, 1]
|
||||
readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1]
|
||||
readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1]
|
||||
readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1]
|
||||
readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1]
|
||||
readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1]
|
||||
readonly property real expressiveFastSpatialDuration: 350
|
||||
readonly property real expressiveDefaultSpatialDuration: 500
|
||||
readonly property real expressiveSlowSpatialDuration: 650
|
||||
readonly property real expressiveEffectsDuration: 200
|
||||
}
|
||||
|
||||
animation: QtObject {
|
||||
property QtObject elementMove: QtObject {
|
||||
property int duration: animationCurves.expressiveDefaultSpatialDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial
|
||||
property int velocity: 650
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMove.duration
|
||||
easing.type: root.animation.elementMove.type
|
||||
easing.bezierCurve: root.animation.elementMove.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveSmall: QtObject {
|
||||
property int duration: animationCurves.expressiveFastSpatialDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.expressiveFastSpatial
|
||||
property int velocity: 650
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
duration: root.animation.elementMoveSmall.duration
|
||||
easing.type: root.animation.elementMoveSmall.type
|
||||
easing.bezierCurve: root.animation.elementMoveSmall.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveEnter: QtObject {
|
||||
property int duration: 400
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.emphasizedDecel
|
||||
property int velocity: 650
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
alwaysRunToEnd: true
|
||||
duration: root.animation.elementMoveEnter.duration
|
||||
easing.type: root.animation.elementMoveEnter.type
|
||||
easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveExit: QtObject {
|
||||
property int duration: 200
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.emphasizedAccel
|
||||
property int velocity: 650
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
alwaysRunToEnd: true
|
||||
duration: root.animation.elementMoveExit.duration
|
||||
easing.type: root.animation.elementMoveExit.type
|
||||
easing.bezierCurve: root.animation.elementMoveExit.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject elementMoveFast: QtObject {
|
||||
property int duration: animationCurves.expressiveEffectsDuration
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.expressiveEffects
|
||||
property int velocity: 850
|
||||
property Component colorAnimation: Component { ColorAnimation {
|
||||
duration: root.animation.elementMoveFast.duration
|
||||
easing.type: root.animation.elementMoveFast.type
|
||||
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
|
||||
}}
|
||||
property Component numberAnimation: Component { NumberAnimation {
|
||||
alwaysRunToEnd: true
|
||||
duration: root.animation.elementMoveFast.duration
|
||||
easing.type: root.animation.elementMoveFast.type
|
||||
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
|
||||
}}
|
||||
}
|
||||
|
||||
property QtObject elementResize: QtObject {
|
||||
property int duration: 300
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.emphasized
|
||||
property int velocity: 650
|
||||
property Component numberAnimation: Component {
|
||||
NumberAnimation {
|
||||
alwaysRunToEnd: true
|
||||
duration: root.animation.elementResize.duration
|
||||
easing.type: root.animation.elementResize.type
|
||||
easing.bezierCurve: root.animation.elementResize.bezierCurve
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
property QtObject clickBounce: QtObject {
|
||||
property int duration: 400
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial
|
||||
property int velocity: 850
|
||||
property Component numberAnimation: Component { NumberAnimation {
|
||||
alwaysRunToEnd: true
|
||||
duration: root.animation.clickBounce.duration
|
||||
easing.type: root.animation.clickBounce.type
|
||||
easing.bezierCurve: root.animation.clickBounce.bezierCurve
|
||||
}}
|
||||
}
|
||||
|
||||
property QtObject scroll: QtObject {
|
||||
property int duration: 200
|
||||
property int type: Easing.BezierSpline
|
||||
property list<real> bezierCurve: root.animationCurves.standardDecel
|
||||
}
|
||||
|
||||
property QtObject menuDecel: QtObject {
|
||||
property int duration: 350
|
||||
property int type: Easing.OutExpo
|
||||
}
|
||||
}
|
||||
|
||||
sizes: QtObject {
|
||||
property real baseBarHeight: 40
|
||||
property real barHeight: Config.options.bar.cornerStyle === 1 ?
|
||||
(baseBarHeight + root.sizes.hyprlandGapsOut * 2) : baseBarHeight
|
||||
property real barCenterSideModuleWidth: Config.options?.bar.verbose ? 360 : 140
|
||||
property real barCenterSideModuleWidthShortened: 280
|
||||
property real barCenterSideModuleWidthHellaShortened: 190
|
||||
property real barShortenScreenWidthThreshold: 1200 // Shorten if screen width is at most this value
|
||||
property real barHellaShortenScreenWidthThreshold: 1000 // Shorten even more...
|
||||
property real elevationMargin: 10
|
||||
property real fabShadowRadius: 5
|
||||
property real fabHoveredShadowRadius: 7
|
||||
property real hyprlandGapsOut: 5
|
||||
property real mediaControlsWidth: 440
|
||||
property real mediaControlsHeight: 160
|
||||
property real notificationPopupWidth: 410
|
||||
property real osdWidth: 180
|
||||
property real searchWidthCollapsed: 210
|
||||
property real searchWidth: 360
|
||||
property real sidebarWidth: Config.options.sidebar.width
|
||||
property real sidebarWidthExtended: Config.options.sidebar.widthExtended
|
||||
property real baseVerticalBarWidth: 46
|
||||
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
|
||||
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
|
||||
property real wallpaperSelectorWidth: 1200
|
||||
property real wallpaperSelectorHeight: 690
|
||||
property real wallpaperSelectorItemMargins: 8
|
||||
property real wallpaperSelectorItemPadding: 6
|
||||
}
|
||||
|
||||
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
|
||||
}
|
||||
686
surfaces/quickshell/ii-base/modules/common/Config.qml
Normal file
686
surfaces/quickshell/ii-base/modules/common/Config.qml
Normal file
|
|
@ -0,0 +1,686 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common.functions
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property string filePath: Directories.shellConfigPath
|
||||
property alias options: configOptionsJsonAdapter
|
||||
property bool ready: false
|
||||
property int readWriteDelay: 50 // milliseconds
|
||||
property bool blockWrites: false
|
||||
|
||||
function setNestedValue(nestedKey, value) {
|
||||
let keys = nestedKey.split(".");
|
||||
let obj = root.options;
|
||||
let parents = [obj];
|
||||
|
||||
// Traverse and collect parent objects
|
||||
for (let i = 0; i < keys.length - 1; ++i) {
|
||||
if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") {
|
||||
obj[keys[i]] = {};
|
||||
}
|
||||
obj = obj[keys[i]];
|
||||
parents.push(obj);
|
||||
}
|
||||
|
||||
// Convert value to correct type using JSON.parse when safe
|
||||
let convertedValue = value;
|
||||
if (typeof value === "string") {
|
||||
let trimmed = value.trim();
|
||||
if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) {
|
||||
try {
|
||||
convertedValue = JSON.parse(trimmed);
|
||||
} catch (e) {
|
||||
convertedValue = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
obj[keys[keys.length - 1]] = convertedValue;
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fileReloadTimer
|
||||
interval: root.readWriteDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
configFileView.reload()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fileWriteTimer
|
||||
interval: root.readWriteDelay
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
configFileView.writeAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: configFileView
|
||||
path: root.filePath
|
||||
watchChanges: true
|
||||
blockWrites: root.blockWrites
|
||||
onFileChanged: fileReloadTimer.restart()
|
||||
onAdapterUpdated: fileWriteTimer.restart()
|
||||
onLoaded: root.ready = true
|
||||
onLoadFailed: error => {
|
||||
if (error == FileViewError.FileNotFound) {
|
||||
writeAdapter();
|
||||
}
|
||||
}
|
||||
|
||||
JsonAdapter {
|
||||
id: configOptionsJsonAdapter
|
||||
|
||||
property string panelFamily: "ii" // "ii", "waffle"
|
||||
|
||||
property JsonObject policies: JsonObject {
|
||||
property int ai: 1 // 0: No | 1: Yes | 2: Local
|
||||
property int weeb: 1 // 0: No | 1: Open | 2: Closet
|
||||
}
|
||||
|
||||
property JsonObject ai: JsonObject {
|
||||
property string systemPrompt: "## Style\n- Use casual tone, don't be formal!\n- Always be brief and to the point, unless asked otherwise\n- Don't repeat the user's question\n- Be approachable: Avoid using overly complicated, domain-specific terms and provide analogies when asked to explain a concept\n\n## Context (ignore when irrelevant)\n- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system\n- Desktop environment: {DE}\n- Current date & time: {DATETIME}\n- Focused app: {WINDOWCLASS}\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n"
|
||||
property string tool: "functions" // search, functions, or none
|
||||
property list<var> extraModels: [
|
||||
{
|
||||
"api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models
|
||||
"description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B",
|
||||
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
|
||||
"homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", // Not mandatory
|
||||
"icon": "spark-symbolic", // Not mandatory
|
||||
"key_get_link": "https://openrouter.ai/settings/keys", // Not mandatory
|
||||
"key_id": "openrouter",
|
||||
"model": "deepseek/deepseek-r1-distill-llama-70b:free",
|
||||
"name": "Custom: DS R1 Dstl. LLaMA 70B",
|
||||
"requires_key": true
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
property JsonObject appearance: JsonObject {
|
||||
property bool extraBackgroundTint: true
|
||||
property int fakeScreenRounding: 2 // 0: None | 1: Always | 2: When not fullscreen
|
||||
property JsonObject fonts: JsonObject {
|
||||
property string main: "Google Sans Flex"
|
||||
property string numbers: "Google Sans Flex"
|
||||
property string title: "Google Sans Flex"
|
||||
property string iconNerd: "JetBrains Mono NF"
|
||||
property string monospace: "JetBrains Mono NF"
|
||||
property string reading: "Readex Pro"
|
||||
property string expressive: "Space Grotesk"
|
||||
}
|
||||
property JsonObject transparency: JsonObject {
|
||||
property bool enable: false
|
||||
property bool automatic: true
|
||||
property real backgroundTransparency: 0.11
|
||||
property real contentTransparency: 0.57
|
||||
}
|
||||
property JsonObject wallpaperTheming: JsonObject {
|
||||
property bool enableAppsAndShell: true
|
||||
property bool enableQtApps: true
|
||||
property bool enableTerminal: true
|
||||
property JsonObject terminalGenerationProps: JsonObject {
|
||||
property real harmony: 0.6
|
||||
property real harmonizeThreshold: 100
|
||||
property real termFgBoost: 0.35
|
||||
property real termBgTone: 0.4
|
||||
property bool forceDarkMode: false
|
||||
}
|
||||
}
|
||||
property JsonObject palette: JsonObject {
|
||||
property string type: "auto" // Allowed: auto, scheme-content, scheme-expressive, scheme-fidelity, scheme-fruit-salad, scheme-monochrome, scheme-neutral, scheme-rainbow, scheme-tonal-spot
|
||||
property string accentColor: ""
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject audio: JsonObject {
|
||||
// Values in %
|
||||
property JsonObject protection: JsonObject {
|
||||
// Prevent sudden bangs
|
||||
property bool enable: false
|
||||
property real maxAllowedIncrease: 10
|
||||
property real maxAllowed: 99
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject apps: JsonObject {
|
||||
property string bluetooth: "kcmshell6 kcm_bluetooth"
|
||||
property string changePassword: "kitty -1 --hold=yes fish -i -c 'passwd'"
|
||||
property string network: "kcmshell6 kcm_networkmanagement"
|
||||
property string manageUser: "kcmshell6 kcm_users"
|
||||
property string networkEthernet: "kcmshell6 kcm_networkmanagement"
|
||||
property string taskManager: "plasma-systemmonitor --page-name Processes"
|
||||
property string terminal: "kitty -1" // This is only for shell actions
|
||||
property string update: "kitty -1 --hold=yes fish -i -c 'pkexec pacman -Syu'"
|
||||
property string volumeMixer: `~/.config/hypr/hyprland/scripts/launch_first_available.sh "pavucontrol-qt" "pavucontrol"`
|
||||
}
|
||||
|
||||
property JsonObject background: JsonObject {
|
||||
property JsonObject widgets: JsonObject {
|
||||
property JsonObject clock: JsonObject {
|
||||
property bool enable: true
|
||||
property bool showOnlyWhenLocked: false
|
||||
property string placementStrategy: "leastBusy" // "free", "leastBusy", "mostBusy"
|
||||
property real x: 100
|
||||
property real y: 100
|
||||
property string style: "cookie" // Options: "cookie", "digital"
|
||||
property string styleLocked: "cookie" // Options: "cookie", "digital"
|
||||
property JsonObject cookie: JsonObject {
|
||||
property bool aiStyling: false
|
||||
property int sides: 14
|
||||
property string dialNumberStyle: "full" // Options: "dots" , "numbers", "full" , "none"
|
||||
property string hourHandStyle: "fill" // Options: "classic", "fill", "hollow", "hide"
|
||||
property string minuteHandStyle: "medium" // Options "classic", "thin", "medium", "bold", "hide"
|
||||
property string secondHandStyle: "dot" // Options: "dot", "line", "classic", "hide"
|
||||
property string dateStyle: "bubble" // Options: "border", "rect", "bubble" , "hide"
|
||||
property bool timeIndicators: true
|
||||
property bool hourMarks: false
|
||||
property bool dateInClock: true
|
||||
property bool constantlyRotate: false
|
||||
property bool useSineCookie: false
|
||||
}
|
||||
property JsonObject digital: JsonObject {
|
||||
property bool adaptiveAlignment: true
|
||||
property bool showDate: true
|
||||
property bool animateChange: true
|
||||
property bool vertical: false
|
||||
property JsonObject font: JsonObject {
|
||||
property string family: "Google Sans Flex"
|
||||
property real weight: 350
|
||||
property real width: 100
|
||||
property real size: 90
|
||||
property real roundness: 0
|
||||
}
|
||||
}
|
||||
property JsonObject quote: JsonObject {
|
||||
property bool enable: false
|
||||
property string text: ""
|
||||
}
|
||||
}
|
||||
property JsonObject weather: JsonObject {
|
||||
property bool enable: false
|
||||
property string placementStrategy: "free" // "free", "leastBusy", "mostBusy"
|
||||
property real x: 400
|
||||
property real y: 100
|
||||
}
|
||||
property JsonObject visualizer: JsonObject {
|
||||
property bool enable: false
|
||||
property string placementStrategy: "free"
|
||||
property real x: 500
|
||||
property real y: 500
|
||||
property int bars: 30
|
||||
property string style: "bars"
|
||||
property bool vertical: false
|
||||
}
|
||||
property JsonObject stats: JsonObject {
|
||||
property bool enable: false
|
||||
property string placementStrategy: "free"
|
||||
property real x: 150
|
||||
property real y: 300
|
||||
property string githubUsername: ""
|
||||
property string codeforcesUsername: ""
|
||||
property bool showGraphs: false
|
||||
}
|
||||
property JsonObject systemResources: JsonObject {
|
||||
property bool enable: false
|
||||
property string placementStrategy: "free"
|
||||
property real x: 150
|
||||
property real y: 600
|
||||
property bool showGraphs: false
|
||||
}
|
||||
}
|
||||
property string wallpaperPath: ""
|
||||
property string thumbnailPath: ""
|
||||
property bool hideWhenFullscreen: true
|
||||
property JsonObject parallax: JsonObject {
|
||||
property bool vertical: false
|
||||
property bool autoVertical: false
|
||||
property bool enableWorkspace: false
|
||||
property real workspaceZoom: 1.07 // Relative to wallpaper size
|
||||
property bool enableSidebar: false
|
||||
property real widgetsFactor: 1.2
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject bar: JsonObject {
|
||||
property JsonObject autoHide: JsonObject {
|
||||
property bool enable: false
|
||||
property int hoverRegionWidth: 2
|
||||
property bool pushWindows: false
|
||||
property JsonObject showWhenPressingSuper: JsonObject {
|
||||
property bool enable: true
|
||||
property int delay: 140
|
||||
property bool suppressSearchOnHold: true
|
||||
property int suppressSearchDelay: 200
|
||||
}
|
||||
}
|
||||
property bool bottom: false // Instead of top
|
||||
property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle
|
||||
property bool floatStyleShadow: true // Show shadow behind bar when cornerStyle == 1 (Float)
|
||||
property bool borderless: false // true for no grouping of items
|
||||
property string topLeftIcon: "spark" // Options: "distro" or any icon name in ~/.config/quickshell/ii/assets/icons
|
||||
property bool showBackground: true
|
||||
property bool verbose: true
|
||||
property bool vertical: false
|
||||
property JsonObject resources: JsonObject {
|
||||
property bool alwaysShowSwap: true
|
||||
property bool alwaysShowCpu: true
|
||||
property int memoryWarningThreshold: 95
|
||||
property int swapWarningThreshold: 85
|
||||
property int cpuWarningThreshold: 90
|
||||
}
|
||||
property list<string> screenList: [] // List of names, like "eDP-1", find out with 'hyprctl monitors' command
|
||||
property JsonObject utilButtons: JsonObject {
|
||||
property bool showScreenSnip: true
|
||||
property bool showColorPicker: false
|
||||
property bool showMicToggle: false
|
||||
property bool showKeyboardToggle: true
|
||||
property bool showDarkModeToggle: true
|
||||
property bool showPerformanceProfileToggle: false
|
||||
property bool showScreenRecord: false
|
||||
}
|
||||
property JsonObject workspaces: JsonObject {
|
||||
property bool monochromeIcons: true
|
||||
property int shown: 10
|
||||
property bool showAppIcons: true
|
||||
property bool alwaysShowNumbers: false
|
||||
property int showNumberDelay: 300 // milliseconds
|
||||
property list<string> numberMap: ["1", "2"] // Characters to show instead of numbers on workspace indicator
|
||||
property bool useNerdFont: false
|
||||
}
|
||||
property JsonObject weather: JsonObject {
|
||||
property bool enable: false
|
||||
property bool enableGPS: true // gps based location
|
||||
property string city: "" // When 'enableGPS' is false
|
||||
property bool useUSCS: false // Instead of metric (SI) units
|
||||
property int fetchInterval: 10 // minutes
|
||||
}
|
||||
property JsonObject claudeUsage: JsonObject {
|
||||
property bool enable: false // Show a Claude (Pro/Max) subscription usage gauge in the bar
|
||||
property bool defaultWeekly: false // Start on the 7-day window; click the gauge to switch session <-> week
|
||||
property int warningThreshold: 90 // Turn the gauge red at/above this utilization (%)
|
||||
property int fetchInterval: 5 // minutes
|
||||
}
|
||||
property JsonObject indicators: JsonObject {
|
||||
property JsonObject notifications: JsonObject {
|
||||
property bool showUnreadCount: false
|
||||
}
|
||||
}
|
||||
property JsonObject tooltips: JsonObject {
|
||||
property bool clickToShow: false
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject battery: JsonObject {
|
||||
property int low: 20
|
||||
property int critical: 5
|
||||
property int full: 101
|
||||
property bool automaticSuspend: true
|
||||
property int suspend: 3
|
||||
}
|
||||
|
||||
property JsonObject calendar: JsonObject {
|
||||
property string locale: "en-GB"
|
||||
}
|
||||
|
||||
property JsonObject cheatsheet: JsonObject {
|
||||
// Use a nerdfont to see the icons
|
||||
// 0: | 1: | 2: | 3: | 4:
|
||||
// 5: | 6: | 7: | 8: | 9:
|
||||
// 10: | 11: | 12: | 13: | 14:
|
||||
property string superKey: ""
|
||||
property bool useMacSymbol: false
|
||||
property bool splitButtons: false
|
||||
property bool useMouseSymbol: false
|
||||
property bool useFnSymbol: false
|
||||
property JsonObject fontSize: JsonObject {
|
||||
property int key: Appearance.font.pixelSize.smaller
|
||||
property int comment: Appearance.font.pixelSize.smaller
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject conflictKiller: JsonObject {
|
||||
property bool autoKillNotificationDaemons: false
|
||||
property bool autoKillTrays: false
|
||||
}
|
||||
|
||||
property JsonObject crosshair: JsonObject {
|
||||
// Valorant crosshair format. Use https://www.vcrdb.net/builder
|
||||
property string code: "0;P;d;1;0l;10;0o;2;1b;0"
|
||||
}
|
||||
|
||||
property JsonObject dock: JsonObject {
|
||||
property bool enable: false
|
||||
property bool monochromeIcons: true
|
||||
property real height: 60
|
||||
property real hoverRegionHeight: 2
|
||||
property bool pinnedOnStartup: false
|
||||
property bool hoverToReveal: true // When false, only reveals on empty workspace
|
||||
property list<string> pinnedApps: [ // IDs of pinned entries
|
||||
"org.kde.dolphin", "kitty",]
|
||||
property list<string> ignoredAppRegexes: []
|
||||
}
|
||||
|
||||
property JsonObject interactions: JsonObject {
|
||||
property JsonObject scrolling: JsonObject {
|
||||
property bool fasterTouchpadScroll: false // Enable faster scrolling with touchpad
|
||||
property int mouseScrollDeltaThreshold: 120 // delta >= this then it gets detected as mouse scroll rather than touchpad
|
||||
property int mouseScrollFactor: 120
|
||||
property int touchpadScrollFactor: 450
|
||||
}
|
||||
property JsonObject deadPixelWorkaround: JsonObject { // Hyprland leaves out 1 pixel on the right for interactions
|
||||
property bool enable: false
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject language: JsonObject {
|
||||
property string ui: "auto" // UI language. "auto" for system locale, or specific language code like "zh_CN", "en_US"
|
||||
property JsonObject translator: JsonObject {
|
||||
property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google
|
||||
property string targetLanguage: "auto" // Run `trans -list-all` for available languages
|
||||
property string sourceLanguage: "auto"
|
||||
property list<string> history: []
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject launcher: JsonObject {
|
||||
property list<string> pinnedApps: [ "org.kde.dolphin", "kitty", "cmake-gui"]
|
||||
}
|
||||
|
||||
property JsonObject light: JsonObject {
|
||||
property JsonObject night: JsonObject {
|
||||
property bool automatic: true
|
||||
property string from: "19:00" // Format: "HH:mm", 24-hour time
|
||||
property string to: "06:30" // Format: "HH:mm", 24-hour time
|
||||
property int colorTemperature: 5000
|
||||
}
|
||||
property JsonObject antiFlashbang: JsonObject {
|
||||
property bool enable: false
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject lock: JsonObject {
|
||||
property bool useHyprlock: false
|
||||
property bool launchOnStartup: false
|
||||
property int dpmsTimeout: 3
|
||||
property string unlockHook: ""
|
||||
property JsonObject blur: JsonObject {
|
||||
property bool enable: true
|
||||
property real radius: 100
|
||||
property real extraZoom: 1.1
|
||||
}
|
||||
property bool centerClock: true
|
||||
property bool showLockedText: true
|
||||
property JsonObject security: JsonObject {
|
||||
property bool unlockKeyring: true
|
||||
property bool requirePasswordToPower: false
|
||||
}
|
||||
property bool materialShapeChars: true
|
||||
}
|
||||
|
||||
property JsonObject media: JsonObject {
|
||||
// Attempt to remove dupes (the aggregator playerctl one and browsers' native ones when there's plasma browser integration)
|
||||
property bool filterDuplicatePlayers: true
|
||||
}
|
||||
|
||||
property JsonObject networking: JsonObject {
|
||||
property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
|
||||
}
|
||||
|
||||
property JsonObject notifications: JsonObject {
|
||||
property int timeout: 7000
|
||||
property JsonObject monitor: JsonObject {
|
||||
property bool enable: false
|
||||
property string name: "" // Name of the monitor to show notifications on, like "eDP-1". Find out with 'hyprctl monitors' command
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject osd: JsonObject {
|
||||
property int timeout: 1000
|
||||
}
|
||||
|
||||
property JsonObject osk: JsonObject {
|
||||
property string layout: "qwerty_full"
|
||||
property bool pinnedOnStartup: false
|
||||
}
|
||||
|
||||
property JsonObject overlay: JsonObject {
|
||||
property bool openingZoomAnimation: true
|
||||
property bool darkenScreen: true
|
||||
property real clickthroughOpacity: 0.8
|
||||
property JsonObject floatingImage: JsonObject {
|
||||
property string imageSource: "https://media.tenor.com/H5U5bJzj3oAAAAAi/kukuru.gif"
|
||||
property real scale: 0.5
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject overview: JsonObject {
|
||||
property bool enable: true
|
||||
property real scale: 0.18 // Relative to screen size
|
||||
property real rows: 2
|
||||
property real columns: 5
|
||||
property bool orderRightLeft: false
|
||||
property bool orderBottomUp: false
|
||||
property bool centerIcons: true
|
||||
}
|
||||
|
||||
property JsonObject regionSelector: JsonObject {
|
||||
property JsonObject targetRegions: JsonObject {
|
||||
property bool windows: true
|
||||
property bool layers: false
|
||||
property bool content: true
|
||||
property bool showLabel: false
|
||||
property real opacity: 0.3
|
||||
property real contentRegionOpacity: 0.8
|
||||
property int selectionPadding: 5
|
||||
}
|
||||
property JsonObject rect: JsonObject {
|
||||
property bool showAimLines: true
|
||||
}
|
||||
property JsonObject circle: JsonObject {
|
||||
property int strokeWidth: 6
|
||||
property int padding: 10
|
||||
}
|
||||
property JsonObject annotation: JsonObject {
|
||||
property bool useSatty: false
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject resources: JsonObject {
|
||||
property int updateInterval: 3000
|
||||
property int historyLength: 60
|
||||
}
|
||||
|
||||
property JsonObject tray: JsonObject {
|
||||
property bool monochromeIcons: true
|
||||
property bool showItemId: false
|
||||
property bool invertPinnedItems: true // Makes the below a whitelist for the tray and blacklist for the pinned area
|
||||
property list<var> pinnedItems: [ "Fcitx" ]
|
||||
property bool filterPassive: true
|
||||
}
|
||||
|
||||
property JsonObject musicRecognition: JsonObject {
|
||||
property int timeout: 16
|
||||
property int interval: 4
|
||||
}
|
||||
|
||||
property bool autoIdleInhibit: false
|
||||
|
||||
property JsonObject search: JsonObject {
|
||||
property int nonAppResultDelay: 30 // This prevents lagging when typing
|
||||
property string engineBaseUrl: "https://www.google.com/search?q="
|
||||
property list<string> excludedSites: ["quora.com", "facebook.com"]
|
||||
property bool sloppy: false // Uses levenshtein distance based scoring instead of fuzzy sort. Very weird.
|
||||
property JsonObject prefix: JsonObject {
|
||||
property bool showDefaultActionsWithoutPrefix: true
|
||||
property string action: "/"
|
||||
property string app: ">"
|
||||
property string clipboard: ";"
|
||||
property string emojis: ":"
|
||||
property string math: "="
|
||||
property string shellCommand: "$"
|
||||
property string webSearch: "?"
|
||||
}
|
||||
property JsonObject imageSearch: JsonObject {
|
||||
property string imageSearchEngineBaseUrl: "https://lens.google.com/uploadbyurl?url="
|
||||
property bool useCircleSelection: false
|
||||
}
|
||||
property JsonObject fileSearch: JsonObject {
|
||||
property bool enable: false
|
||||
property list<string> paths: [Directories.home]
|
||||
property list<string> exclude: [".git", "node_modules", "target", "build"]
|
||||
property bool excludeHiddenDirs: true
|
||||
property int maxResults: 30
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject sidebar: JsonObject {
|
||||
property bool keepRightSidebarLoaded: true
|
||||
property int width: 460 // Collapsed sidebar width in px
|
||||
property int widthExtended: 750 // Extended (Ctrl+O) sidebar width in px
|
||||
property JsonObject translator: JsonObject {
|
||||
property bool enable: false
|
||||
property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag.
|
||||
}
|
||||
property JsonObject ai: JsonObject {
|
||||
property bool textFadeIn: false
|
||||
// Chat message text size. Defaults to the theme value (live
|
||||
// binding, like cheatsheet.fontSize) so a theme change tracks
|
||||
// automatically instead of pinning a literal.
|
||||
property int fontSize: Appearance.font.pixelSize.small
|
||||
}
|
||||
property JsonObject booru: JsonObject {
|
||||
property bool allowNsfw: false
|
||||
property string defaultProvider: "yandere"
|
||||
property int limit: 20
|
||||
property JsonObject zerochan: JsonObject {
|
||||
property string username: "[unset]"
|
||||
}
|
||||
}
|
||||
property JsonObject cornerOpen: JsonObject {
|
||||
property bool enable: true
|
||||
property bool bottom: false
|
||||
property bool valueScroll: true
|
||||
property bool clickless: false
|
||||
property int cornerRegionWidth: 250
|
||||
property int cornerRegionHeight: 5
|
||||
property bool visualize: false
|
||||
property bool clicklessCornerEnd: true
|
||||
property int clicklessCornerVerticalOffset: 1
|
||||
}
|
||||
|
||||
property JsonObject quickToggles: JsonObject {
|
||||
property string style: "android" // Options: classic, android
|
||||
property JsonObject android: JsonObject {
|
||||
property int columns: 5
|
||||
property list<var> toggles: [
|
||||
{ "size": 2, "type": "network" },
|
||||
{ "size": 2, "type": "bluetooth" },
|
||||
{ "size": 1, "type": "idleInhibitor" },
|
||||
{ "size": 1, "type": "mic" },
|
||||
{ "size": 2, "type": "audio" },
|
||||
{ "size": 2, "type": "nightLight" }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject quickSliders: JsonObject {
|
||||
property bool enable: false
|
||||
property bool showMic: false
|
||||
property bool showVolume: true
|
||||
property bool showBrightness: true
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject screenRecord: JsonObject {
|
||||
property string savePath: Directories.videos.replace("file://","") // strip "file://"
|
||||
}
|
||||
|
||||
property JsonObject screenSnip: JsonObject {
|
||||
property string savePath: "" // only copy to clipboard when empty
|
||||
}
|
||||
|
||||
property JsonObject sounds: JsonObject {
|
||||
property bool battery: false
|
||||
property bool pomodoro: false
|
||||
property string theme: "freedesktop"
|
||||
}
|
||||
|
||||
property JsonObject time: JsonObject {
|
||||
// https://doc.qt.io/qt-6/qtime.html#toString
|
||||
property string format: "hh:mm"
|
||||
property string shortDateFormat: "dd/MM"
|
||||
property string dateWithYearFormat: "dd/MM/yyyy"
|
||||
property string dateFormat: "ddd, dd/MM"
|
||||
property JsonObject pomodoro: JsonObject {
|
||||
property int breakTime: 300
|
||||
property int cyclesBeforeLongBreak: 4
|
||||
property int focus: 1500
|
||||
property int longBreak: 900
|
||||
property bool notifications: true
|
||||
}
|
||||
property bool secondPrecision: false
|
||||
}
|
||||
|
||||
property JsonObject updates: JsonObject {
|
||||
property bool enableCheck: true
|
||||
property int checkInterval: 120 // minutes
|
||||
property int adviseUpdateThreshold: 75 // packages
|
||||
property int stronglyAdviseUpdateThreshold: 200 // packages
|
||||
}
|
||||
|
||||
property JsonObject wallpaperSelector: JsonObject {
|
||||
property bool useSystemFileDialog: false
|
||||
}
|
||||
|
||||
property JsonObject windows: JsonObject {
|
||||
property bool showTitlebar: true // Client-side decoration for shell apps
|
||||
property bool centerTitle: true
|
||||
}
|
||||
|
||||
property JsonObject hacks: JsonObject {
|
||||
property int arbitraryRaceConditionDelay: 20 // milliseconds
|
||||
}
|
||||
|
||||
property JsonObject workSafety: JsonObject {
|
||||
property JsonObject enable: JsonObject {
|
||||
property bool wallpaper: false
|
||||
property bool clipboard: false
|
||||
}
|
||||
property JsonObject triggerCondition: JsonObject {
|
||||
property list<string> networkNameKeywords: ["airport", "cafe", "college", "company", "eduroam", "free", "guest", "public", "school", "university"]
|
||||
property list<string> fileKeywords: ["anime", "booru", "ecchi", "hentai", "yande.re", "konachan", "breast", "nipples", "pussy", "nsfw", "spoiler", "girl"]
|
||||
property list<string> linkKeywords: ["hentai", "porn", "sukebei", "hitomi.la", "rule34", "gelbooru", "fanbox", "dlsite"]
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject waffles: JsonObject {
|
||||
// Some spots are kinda janky/awkward. Setting the following to
|
||||
// false will make (some) stuff also be like that for accuracy.
|
||||
// Example: the right-click menu of the Start button
|
||||
property JsonObject tweaks: JsonObject {
|
||||
property bool switchHandlePositionFix: true
|
||||
property bool smootherMenuAnimations: true
|
||||
property bool smootherSearchBar: true
|
||||
}
|
||||
property JsonObject bar: JsonObject {
|
||||
property bool bottom: true
|
||||
property bool leftAlignApps: false
|
||||
}
|
||||
property JsonObject actionCenter: JsonObject {
|
||||
property list<string> toggles: [ "network", "bluetooth", "easyEffects", "powerProfile", "idleInhibitor", "nightLight", "darkMode", "antiFlashbang", "cloudflareWarp", "mic", "musicRecognition", "notifications", "onScreenKeyboard", "gameMode", "screenSnip", "colorPicker" ]
|
||||
}
|
||||
property JsonObject calendar: JsonObject {
|
||||
property bool force2CharDayOfWeek: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
66
surfaces/quickshell/ii-base/modules/common/Directories.qml
Normal file
66
surfaces/quickshell/ii-base/modules/common/Directories.qml
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import QtCore
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
// XDG Dirs, with "file://"
|
||||
readonly property string home: StandardPaths.standardLocations(StandardPaths.HomeLocation)[0]
|
||||
readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0]
|
||||
readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0]
|
||||
readonly property string cache: StandardPaths.standardLocations(StandardPaths.CacheLocation)[0]
|
||||
readonly property string genericCache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0]
|
||||
readonly property string documents: StandardPaths.standardLocations(StandardPaths.DocumentsLocation)[0]
|
||||
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
|
||||
readonly property string pictures: StandardPaths.standardLocations(StandardPaths.PicturesLocation)[0]
|
||||
readonly property string music: StandardPaths.standardLocations(StandardPaths.MusicLocation)[0]
|
||||
readonly property string videos: StandardPaths.standardLocations(StandardPaths.MoviesLocation)[0]
|
||||
|
||||
// Other dirs used by the shell, without "file://"
|
||||
property string assetsPath: Quickshell.shellPath("assets")
|
||||
property string scriptPath: Quickshell.shellPath("scripts")
|
||||
property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`)
|
||||
property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`)
|
||||
property string tempImages: "/tmp/quickshell/media/images"
|
||||
property string booruPreviews: FileUtils.trimFileProtocol(`${Directories.cache}/media/boorus`)
|
||||
property string booruDownloads: FileUtils.trimFileProtocol(Directories.pictures + "/homework")
|
||||
property string booruDownloadsNsfw: FileUtils.trimFileProtocol(Directories.pictures + "/homework/🌶️")
|
||||
property string latexOutput: FileUtils.trimFileProtocol(`${Directories.cache}/media/latex`)
|
||||
property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/illogical-impulse`)
|
||||
property string shellConfigName: "config.json"
|
||||
property string shellConfigPath: `${Directories.shellConfig}/${Directories.shellConfigName}`
|
||||
property string todoPath: FileUtils.trimFileProtocol(`${Directories.state}/user/todo.json`)
|
||||
property string notesPath: FileUtils.trimFileProtocol(`${Directories.state}/user/notes.txt`)
|
||||
property string conflictCachePath: FileUtils.trimFileProtocol(`${Directories.cache}/conflict-killer`)
|
||||
property string notificationsPath: FileUtils.trimFileProtocol(`${Directories.cache}/notifications/notifications.json`)
|
||||
property string generatedMaterialThemePath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/colors.json`)
|
||||
property string generatedWallpaperCategoryPath: FileUtils.trimFileProtocol(`${Directories.state}/user/generated/wallpaper/category.txt`)
|
||||
property string cliphistDecode: FileUtils.trimFileProtocol(`/tmp/quickshell/media/cliphist`)
|
||||
property string screenshotTemp: "/tmp/quickshell/media/screenshot"
|
||||
property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`)
|
||||
property string defaultAiPrompts: Quickshell.shellPath("defaults/ai/prompts")
|
||||
property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`)
|
||||
property string userActions: FileUtils.trimFileProtocol(`${Directories.shellConfig}/actions`)
|
||||
property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`)
|
||||
property string aiTranslationScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/ai/gemini-translate.sh`)
|
||||
property string recordScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/videos/record.sh`)
|
||||
property string userAvatarPathAccountsService: FileUtils.trimFileProtocol(`/var/lib/AccountsService/icons/${SystemInfo.username}`)
|
||||
property string userAvatarPathRicersAndWeirdSystems: FileUtils.trimFileProtocol(`${Directories.home}.face`)
|
||||
property string userAvatarPathRicersAndWeirdSystems2: FileUtils.trimFileProtocol(`${Directories.home}.face.icon`)
|
||||
// Cleanup on init
|
||||
Component.onCompleted: {
|
||||
Quickshell.execDetached(["mkdir", "-p", `${shellConfig}`])
|
||||
Quickshell.execDetached(["mkdir", "-p", `${favicons}`])
|
||||
Quickshell.execDetached(["bash", "-c", `rm -rf '${coverArt}'; mkdir -p '${coverArt}'`])
|
||||
Quickshell.execDetached(["bash", "-c", `rm -rf '${booruPreviews}'; mkdir -p '${booruPreviews}'`])
|
||||
Quickshell.execDetached(["bash", "-c", `rm -rf '${latexOutput}'; mkdir -p '${latexOutput}'`])
|
||||
Quickshell.execDetached(["bash", "-c", `rm -rf '${cliphistDecode}'; mkdir -p '${cliphistDecode}'`])
|
||||
Quickshell.execDetached(["mkdir", "-p", `${aiChats}`])
|
||||
Quickshell.execDetached(["mkdir", "-p", `${userActions}`])
|
||||
Quickshell.execDetached(["rm", "-rf", `${tempImages}`])
|
||||
}
|
||||
}
|
||||
112
surfaces/quickshell/ii-base/modules/common/Icons.qml
Normal file
112
surfaces/quickshell/ii-base/modules/common/Icons.qml
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
pragma Singleton
|
||||
|
||||
// From https://github.com/caelestia-dots/shell (GPLv3)
|
||||
|
||||
import Quickshell
|
||||
import qs.services
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getBatteryIcon(percentage: int): string {
|
||||
if (percentage >= 93) return "battery_android_full";
|
||||
if (percentage >= 78) return "battery_android_6";
|
||||
if (percentage >= 64) return "battery_android_5";
|
||||
if (percentage >= 50) return "battery_android_4";
|
||||
if (percentage >= 35) return "battery_android_3";
|
||||
if (percentage >= 21) return "battery_android_2";
|
||||
if (percentage >= 7) return "battery_android_1";
|
||||
return "battery_android_0";
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
|
||||
function getNetworkMaterialSymbol() {
|
||||
if (Network.ethernet) return "lan";
|
||||
if (Network.wifiEnabled && Network.wifiStatus === "connected") {
|
||||
const strength = Network.active?.strength ?? 0
|
||||
if (strength > 83) return "signal_wifi_4_bar";
|
||||
if (strength > 67) return "network_wifi";
|
||||
if (strength > 50) return "network_wifi_3_bar";
|
||||
if (strength > 33) return "network_wifi_2_bar";
|
||||
if (strength > 17) return "network_wifi_1_bar";
|
||||
return "signal_wifi_0_bar"
|
||||
} else {
|
||||
if (Network.wifiStatus === "connecting") return "signal_wifi_statusbar_not_connected";
|
||||
if (Network.wifiStatus === "disconnected") return "wifi_find";
|
||||
if (Network.wifiStatus === "disabled") return "signal_wifi_off";
|
||||
return "signal_wifi_bad";
|
||||
}
|
||||
}
|
||||
|
||||
readonly property var weatherIconMap: ({
|
||||
"113": "clear_day",
|
||||
"116": "partly_cloudy_day",
|
||||
"119": "cloud",
|
||||
"122": "cloud",
|
||||
"143": "foggy",
|
||||
"176": "rainy",
|
||||
"179": "rainy",
|
||||
"182": "rainy",
|
||||
"185": "rainy",
|
||||
"200": "thunderstorm",
|
||||
"227": "cloudy_snowing",
|
||||
"230": "snowing_heavy",
|
||||
"248": "foggy",
|
||||
"260": "foggy",
|
||||
"263": "rainy",
|
||||
"266": "rainy",
|
||||
"281": "rainy",
|
||||
"284": "rainy",
|
||||
"293": "rainy",
|
||||
"296": "rainy",
|
||||
"299": "rainy",
|
||||
"302": "weather_hail",
|
||||
"305": "rainy",
|
||||
"308": "weather_hail",
|
||||
"311": "rainy",
|
||||
"314": "rainy",
|
||||
"317": "rainy",
|
||||
"320": "cloudy_snowing",
|
||||
"323": "cloudy_snowing",
|
||||
"326": "cloudy_snowing",
|
||||
"329": "snowing_heavy",
|
||||
"332": "snowing_heavy",
|
||||
"335": "snowing",
|
||||
"338": "snowing_heavy",
|
||||
"350": "rainy",
|
||||
"353": "rainy",
|
||||
"356": "rainy",
|
||||
"359": "weather_hail",
|
||||
"362": "rainy",
|
||||
"365": "rainy",
|
||||
"368": "cloudy_snowing",
|
||||
"371": "snowing",
|
||||
"374": "rainy",
|
||||
"377": "rainy",
|
||||
"386": "thunderstorm",
|
||||
"389": "thunderstorm",
|
||||
"392": "thunderstorm",
|
||||
"395": "snowing"
|
||||
})
|
||||
|
||||
|
||||
function getWeatherIcon(code) {
|
||||
const key = String(code)
|
||||
if (weatherIconMap.hasOwnProperty(key)) {
|
||||
return weatherIconMap[key]
|
||||
}
|
||||
}
|
||||
}
|
||||
31
surfaces/quickshell/ii-base/modules/common/Images.qml
Normal file
31
surfaces/quickshell/ii-base/modules/common/Images.qml
Normal file
|
|
@ -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";
|
||||
}
|
||||
}
|
||||
163
surfaces/quickshell/ii-base/modules/common/Persistent.qml
Normal file
163
surfaces/quickshell/ii-base/modules/common/Persistent.qml
Normal file
|
|
@ -0,0 +1,163 @@
|
|||
pragma Singleton
|
||||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
property alias states: persistentStatesJsonAdapter
|
||||
property string fileDir: Directories.state
|
||||
property string fileName: "states.json"
|
||||
property string filePath: `${root.fileDir}/${root.fileName}`
|
||||
|
||||
property bool ready: false
|
||||
property string previousHyprlandInstanceSignature: ""
|
||||
property bool isNewHyprlandInstance: previousHyprlandInstanceSignature !== states.hyprlandInstanceSignature
|
||||
|
||||
onReadyChanged: {
|
||||
root.previousHyprlandInstanceSignature = root.states.hyprlandInstanceSignature
|
||||
root.states.hyprlandInstanceSignature = Quickshell.env("HYPRLAND_INSTANCE_SIGNATURE") || ""
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fileReloadTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
persistentStatesFileView.reload()
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: fileWriteTimer
|
||||
interval: 100
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
persistentStatesFileView.writeAdapter()
|
||||
}
|
||||
}
|
||||
|
||||
FileView {
|
||||
id: persistentStatesFileView
|
||||
path: root.filePath
|
||||
|
||||
watchChanges: true
|
||||
onFileChanged: fileReloadTimer.restart()
|
||||
onAdapterUpdated: fileWriteTimer.restart()
|
||||
onLoaded: root.ready = true
|
||||
onLoadFailed: error => {
|
||||
console.log("Failed to load persistent states file:", error);
|
||||
if (error == FileViewError.FileNotFound) {
|
||||
fileWriteTimer.restart();
|
||||
}
|
||||
}
|
||||
|
||||
adapter: JsonAdapter {
|
||||
id: persistentStatesJsonAdapter
|
||||
|
||||
property string hyprlandInstanceSignature: ""
|
||||
|
||||
property JsonObject ai: JsonObject {
|
||||
property string model: "gemini-2.5-flash"
|
||||
property real temperature: 0.5
|
||||
}
|
||||
|
||||
property JsonObject cheatsheet: JsonObject {
|
||||
property int tabIndex: 0
|
||||
}
|
||||
|
||||
property JsonObject sidebar: JsonObject {
|
||||
property JsonObject bottomGroup: JsonObject {
|
||||
property bool collapsed: false
|
||||
property int tab: 0
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject booru: JsonObject {
|
||||
property bool allowNsfw: false
|
||||
property string provider: "yandere"
|
||||
}
|
||||
|
||||
property JsonObject idle: JsonObject {
|
||||
property bool inhibit: false
|
||||
}
|
||||
|
||||
property JsonObject overlay: JsonObject {
|
||||
property list<string> open: ["crosshair", "recorder", "volumeMixer", "resources"]
|
||||
property JsonObject crosshair: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: true
|
||||
property real x: 827
|
||||
property real y: 441
|
||||
property real width: 250
|
||||
property real height: 100
|
||||
}
|
||||
property JsonObject floatingImage: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 1650
|
||||
property real y: 390
|
||||
property real width: 0
|
||||
property real height: 0
|
||||
}
|
||||
property JsonObject fpsLimiter: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 1570
|
||||
property real y: 615
|
||||
property real width: 280
|
||||
property real height: 80
|
||||
}
|
||||
property JsonObject recorder: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 80
|
||||
property real y: 80
|
||||
property real width: 350
|
||||
property real height: 130
|
||||
}
|
||||
property JsonObject resources: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: true
|
||||
property real x: 1500
|
||||
property real y: 770
|
||||
property real width: 350
|
||||
property real height: 200
|
||||
property int tabIndex: 0
|
||||
}
|
||||
property JsonObject volumeMixer: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: false
|
||||
property real x: 80
|
||||
property real y: 280
|
||||
property real width: 350
|
||||
property real height: 600
|
||||
property int tabIndex: 0
|
||||
}
|
||||
property JsonObject notes: JsonObject {
|
||||
property bool pinned: false
|
||||
property bool clickthrough: true
|
||||
property real x: 1400
|
||||
property real y: 42
|
||||
property real width: 460
|
||||
property real height: 330
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject timer: JsonObject {
|
||||
property JsonObject pomodoro: JsonObject {
|
||||
property bool running: false
|
||||
property int start: 0
|
||||
property bool isBreak: false
|
||||
property int cycle: 0
|
||||
}
|
||||
property JsonObject stopwatch: JsonObject {
|
||||
property bool running: false
|
||||
property int start: 0
|
||||
property list<var> laps: []
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
import "myers.js" as Myers
|
||||
|
||||
/**
|
||||
* Bit-parallel fuzzy matching helpers.
|
||||
*/
|
||||
|
||||
Singleton {
|
||||
/** Lowercase + NFC normalize a string. Same transform applied internally by prepare() and score(). */
|
||||
function normalize(value) { return Myers.normalize(value) }
|
||||
|
||||
/** Precompute pattern state for reuse across comparisons. */
|
||||
function prepare(pattern) { return Myers.prepare(pattern) }
|
||||
|
||||
/** Return Levenshtein edit distance. Normalizes text automatically. */
|
||||
function distance(prepared, text) { return Myers.distance(prepared, text) }
|
||||
|
||||
/** Return Levenshtein edit distance. text must already be normalized. */
|
||||
function distanceNormalized(prepared, text) { return Myers.distanceNormalized(prepared, text) }
|
||||
|
||||
/** Return normalized similarity in [0, 1]. */
|
||||
function score(prepared, text) { return Myers.score(prepared, text) }
|
||||
|
||||
/** Compute one-off similarity without prepare(). */
|
||||
function computeScore(pattern, text) { return Myers.computeScore(pattern, text) }
|
||||
|
||||
/** Compatibility wrapper for Levendist.computeTextMatchScore. */
|
||||
function computeTextMatchScore(s1, s2) { return Myers.computeTextMatchScore(s1, s2) }
|
||||
|
||||
/**
|
||||
* Return candidates sorted by descending similarity.
|
||||
* opts: { key?: string, threshold?: number }
|
||||
*/
|
||||
function search(prepared, candidates, opts) { return Myers.search(prepared, candidates, opts) }
|
||||
}
|
||||
|
|
@ -0,0 +1,174 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* Returns a color with the hue of color2 and the saturation, value, and alpha of color1.
|
||||
*
|
||||
* @param {string} color1 - The base color (any Qt.color-compatible string).
|
||||
* @param {string} color2 - The color to take hue from.
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function colorWithHueOf(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
|
||||
// Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1
|
||||
var hue = c2.hsvHue;
|
||||
var sat = c1.hsvSaturation;
|
||||
var val = c1.hsvValue;
|
||||
var alpha = c1.a;
|
||||
|
||||
return Qt.hsva(hue, sat, val, alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a color with the saturation of color2 and the hue/value/alpha of color1.
|
||||
*
|
||||
* @param {string} color1 - The base color (any Qt.color-compatible string).
|
||||
* @param {string} color2 - The color to take saturation from.
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function colorWithSaturationOf(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
|
||||
var hue = c1.hsvHue;
|
||||
var sat = c2.hsvSaturation;
|
||||
var val = c1.hsvValue;
|
||||
var alpha = c1.a;
|
||||
|
||||
return Qt.hsva(hue, sat, val, alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a color with the given lightness and the hue, saturation, and alpha of the input color (using HSL).
|
||||
*
|
||||
* @param {string} color - The base color (any Qt.color-compatible string).
|
||||
* @param {number} lightness - The lightness value to use (0-1).
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function colorWithLightness(color, lightness) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.hsla(c.hslHue, c.hslSaturation, lightness, c.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a color with the lightness of color2 and the hue, saturation, and alpha of color1 (using HSL).
|
||||
*
|
||||
* @param {string} color1 - The base color (any Qt.color-compatible string).
|
||||
* @param {string} color2 - The color to take lightness from.
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function colorWithLightnessOf(color1, color2) {
|
||||
var c2 = Qt.color(color2);
|
||||
return colorWithLightness(color1, c2.hslLightness);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1.
|
||||
*
|
||||
* @param {string} color1 - The base color (any Qt.color-compatible string).
|
||||
* @param {string} color2 - The accent color.
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function adaptToAccent(color1, color2) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
|
||||
var hue = c2.hslHue;
|
||||
var sat = c2.hslSaturation;
|
||||
var light = c1.hslLightness;
|
||||
var alpha = c1.a;
|
||||
|
||||
return Qt.hsla(hue, sat, light, alpha);
|
||||
}
|
||||
|
||||
/**
|
||||
* Mixes two colors by a given percentage.
|
||||
*
|
||||
* @param {string} color1 - The first color (any Qt.color-compatible string).
|
||||
* @param {string} color2 - The second color.
|
||||
* @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2.
|
||||
* @returns {Qt.rgba} The resulting mixed color.
|
||||
*/
|
||||
function mix(color1, color2, percentage = 0.5) {
|
||||
var c1 = Qt.color(color1);
|
||||
var c2 = Qt.color(color2);
|
||||
return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transparentizes a color by a given percentage.
|
||||
*
|
||||
* @param {string} color - The color (any Qt.color-compatible string).
|
||||
* @param {number} percentage - The amount to transparentize (0-1).
|
||||
* @returns {Qt.rgba} The resulting color.
|
||||
*/
|
||||
function transparentize(color, percentage = 1) {
|
||||
var c = Qt.color(color);
|
||||
return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the alpha channel of a color.
|
||||
*
|
||||
* @param {string} color - The base color (any Qt.color-compatible string).
|
||||
* @param {number} alpha - The desired alpha (0-1).
|
||||
* @returns {Qt.rgba} The resulting color with applied alpha.
|
||||
*/
|
||||
function applyAlpha(color, alpha) {
|
||||
var c = Qt.color(color);
|
||||
var a = Math.max(0, Math.min(1, alpha));
|
||||
return Qt.rgba(c.r, c.g, c.b, a);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the color is considered "dark" (hslLightness < 0.5).
|
||||
*
|
||||
* @param {string} color - The color to check (any Qt.color-compatible string).
|
||||
* @returns {boolean} True if dark, false otherwise.
|
||||
*/
|
||||
function isDark(color) {
|
||||
var c = Qt.color(color);
|
||||
return c.hslLightness < 0.5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clamps a value to the inclusive range [0, 1].
|
||||
*
|
||||
* @param {number} x - The value to clamp.
|
||||
* @returns {number} The clamped value in the range [0, 1].
|
||||
*/
|
||||
function clamp01(x) {
|
||||
return Math.min(1, Math.max(0, x));
|
||||
}
|
||||
|
||||
/**
|
||||
* Solves for the solid overlay color that, when composited over a base color
|
||||
* with a given opacity, yields the target color.
|
||||
*
|
||||
* The compositing equation is:
|
||||
* result = overlay * overlayOpacity + base * (1 - overlayOpacity)
|
||||
*
|
||||
* This function algebraically inverts that equation per channel.
|
||||
*
|
||||
* @param {Qt.rgba} baseColor - The base (background) color.
|
||||
* @param {Qt.rgba} targetColor - The resulting color after compositing.
|
||||
* @param {number} overlayOpacity - The overlay opacity (0-1).
|
||||
* @returns {Qt.rgba} The solved overlay color
|
||||
*/
|
||||
function solveOverlayColor(baseColor, targetColor, overlayOpacity) {
|
||||
const bc = Qt.color(baseColor);
|
||||
const tc = Qt.color(targetColor);
|
||||
let invA = 1.0 - overlayOpacity;
|
||||
|
||||
let r = (tc.r - bc.r * invA) / overlayOpacity;
|
||||
let g = (tc.g - bc.g * invA) / overlayOpacity;
|
||||
let b = (tc.b - bc.b * invA) / overlayOpacity;
|
||||
|
||||
return Qt.rgba(clamp01(r), clamp01(g), clamp01(b), overlayOpacity);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function getFirstDayOfWeek(date, firstDay = 1) {
|
||||
const d = new Date(date); // Copy
|
||||
const day = d.getDay(); // 0 = Sunday, 1 = Monday, ..., 6 = Saturday
|
||||
|
||||
// Calculate difference to firstDay
|
||||
const diff = (day - firstDay + 7) % 7;
|
||||
d.setDate(d.getDate() - diff);
|
||||
return d;
|
||||
}
|
||||
|
||||
function sameDate(d1, d2) {
|
||||
return (d1.getFullYear() === d2.getFullYear() && d1.getMonth() === d2.getMonth() && d1.getDate() === d2.getDate());
|
||||
}
|
||||
|
||||
function getIthDayDateOfSameWeek(date, i, firstDay = 1) {
|
||||
const firstDayDate = root.getFirstDayOfWeek(date, firstDay);
|
||||
const targetDate = new Date(firstDayDate);
|
||||
targetDate.setDate(firstDayDate.getDate() + i);
|
||||
return targetDate;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* Trims the File protocol off the input string
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function trimFileProtocol(str) {
|
||||
let s = str;
|
||||
if (typeof s !== "string") s = str.toString(); // Convert to string if it's an url or whatever
|
||||
return s.startsWith("file://") ? s.slice(7) : s;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the file name from a file path
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function fileNameForPath(str) {
|
||||
if (typeof str !== "string") return "";
|
||||
const trimmed = trimFileProtocol(str);
|
||||
return trimmed.split(/[\\/]/).pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the folder name from a directory path
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function folderNameForPath(str) {
|
||||
if (typeof str !== "string") return "";
|
||||
const trimmed = trimFileProtocol(str);
|
||||
// Remove trailing slash if present
|
||||
const noTrailing = trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed;
|
||||
if (!noTrailing) return "";
|
||||
return noTrailing.split(/[\\/]/).pop();
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the file extension from a file path or name
|
||||
* @param {string} str
|
||||
* @returns {string}
|
||||
*/
|
||||
function trimFileExt(str) {
|
||||
if (typeof str !== "string") return "";
|
||||
const trimmed = trimFileProtocol(str);
|
||||
const lastDot = trimmed.lastIndexOf(".");
|
||||
if (lastDot > -1 && lastDot > trimmed.lastIndexOf("/")) {
|
||||
return trimmed.slice(0, lastDot);
|
||||
}
|
||||
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("/");
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
import "fuzzysort.js" as FuzzySort
|
||||
|
||||
/**
|
||||
* Wrapper for FuzzySort to play nicely with Quickshell's imports
|
||||
*/
|
||||
|
||||
Singleton {
|
||||
function go(...args) {
|
||||
return FuzzySort.go(...args)
|
||||
}
|
||||
|
||||
function prepare(...args) {
|
||||
return FuzzySort.prepare(...args)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
/**
|
||||
* Layout-aware search helpers.
|
||||
*/
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
// Physical layout maps. Bidirectional pairs are generated in the IIFE below.
|
||||
readonly property var _layouts: (function() {
|
||||
const list = [
|
||||
{
|
||||
name: "uk",
|
||||
map: {
|
||||
"й":"q","ц":"w","у":"e","к":"r","е":"t","н":"y","г":"u",
|
||||
"ш":"i","щ":"o","з":"p","х":"[","ї":"]",
|
||||
"Й":"Q","Ц":"W","У":"E","К":"R","Е":"T","Н":"Y","Г":"U",
|
||||
"Ш":"I","Щ":"O","З":"P","Х":"{","Ї":"}",
|
||||
"ф":"a","і":"s","в":"d","а":"f","п":"g","р":"h","о":"j",
|
||||
"л":"k","д":"l","ж":";","є":"'",
|
||||
"Ф":"A","І":"S","В":"D","А":"F","П":"G","Р":"H","О":"J",
|
||||
"Л":"K","Д":"L","Ж":":","Є":'"',
|
||||
"я":"z","ч":"x","с":"c","м":"v","и":"b","т":"n","ь":"m",
|
||||
"б":",","ю":".",
|
||||
"Я":"Z","Ч":"X","С":"C","М":"V","И":"B","Т":"N","Ь":"M",
|
||||
"Б":"<","Ю":">"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "de",
|
||||
map: {
|
||||
"y":"z","Y":"Z","z":"y","Z":"Y",
|
||||
"ü":"[","Ü":"{","ö":";","Ö":":","ä":"'","Ä":'"',"ß":"-"
|
||||
}
|
||||
},
|
||||
{
|
||||
name: "fr",
|
||||
map: {
|
||||
"a":"q","A":"Q","z":"w","Z":"W","q":"a","Q":"A","w":"z","W":"Z",
|
||||
"é":"2","è":"7","ê":"[","à":"0","ù":"`","ç":"9","œ":"p",
|
||||
"m":";","M":":",";":"m",":":"M"
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
// Make all layout maps bidirectional (for example, add "q":"й" for "uk").
|
||||
for (const layout of list) {
|
||||
const reversePairs = {};
|
||||
for (const [key, value] of Object.entries(layout.map)) {
|
||||
if (layout.map[value] === undefined) {
|
||||
reversePairs[value] = key;
|
||||
}
|
||||
}
|
||||
Object.assign(layout.map, reversePairs);
|
||||
}
|
||||
|
||||
return list;
|
||||
})()
|
||||
|
||||
// Compact Cyrillic-to-Latin transliteration for search.
|
||||
readonly property var _cyrillicToLatin: ({
|
||||
"а":"a","б":"b","в":"v","г":"g","ґ":"g",
|
||||
"д":"d","е":"e","є":"e","ж":"z","з":"z",
|
||||
"и":"i","і":"i","ї":"i","й":"y","к":"k",
|
||||
"л":"l","м":"m","н":"n","о":"o","п":"p",
|
||||
"р":"r","с":"s","т":"t","у":"u","ф":"f",
|
||||
"х":"h","ц":"c","ч":"c","ш":"s","щ":"s",
|
||||
"ь":"","ъ":"","ю":"u","я":"a",
|
||||
"ы":"y","э":"e","ё":"o",
|
||||
"А":"A","Б":"B","В":"V","Г":"G","Ґ":"G",
|
||||
"Д":"D","Е":"E","Є":"E","Ж":"Z","З":"Z",
|
||||
"И":"I","І":"I","Ї":"I","Й":"Y","К":"K",
|
||||
"Л":"L","М":"M","Н":"N","О":"O","П":"P",
|
||||
"Р":"R","С":"S","Т":"T","У":"U","Ф":"F",
|
||||
"Х":"H","Ц":"C","Ч":"C","Ш":"S","Щ":"S",
|
||||
"Ь":"","Ъ":"","Ю":"U","Я":"A",
|
||||
"Ы":"Y","Э":"E","Ё":"O"
|
||||
})
|
||||
|
||||
// Internal character mapping helper.
|
||||
function _mapWithLayout(str, layoutName) {
|
||||
const layout = root._layouts.find(l => l.name === layoutName);
|
||||
if (!layout) return str;
|
||||
return str.split("").map(ch => {
|
||||
const mapped = layout.map[ch];
|
||||
return mapped !== undefined ? mapped : ch;
|
||||
}).join("");
|
||||
}
|
||||
|
||||
/**
|
||||
* Return unique layout-corrected variants.
|
||||
* Works in both directions for all configured layouts.
|
||||
*/
|
||||
function translateAll(str) {
|
||||
if (!str) return [];
|
||||
|
||||
const results = [];
|
||||
const seen = new Set([str]); // Keep input out of final results.
|
||||
|
||||
for (const layout of root._layouts) {
|
||||
const translated = root._mapWithLayout(str, layout.name);
|
||||
if (!seen.has(translated)) {
|
||||
seen.add(translated);
|
||||
results.push(translated);
|
||||
}
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return transliterated Latin text.
|
||||
* Return null when input contains no Cyrillic characters.
|
||||
*/
|
||||
function transliterate(str) {
|
||||
if (!str || !/[Ѐ-ӿ]/.test(str)) return null;
|
||||
const table = root._cyrillicToLatin;
|
||||
return str.split("").map(ch => {
|
||||
const mapped = table[ch];
|
||||
return mapped !== undefined ? mapped : ch;
|
||||
}).join("").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
import "levendist.js" as Levendist
|
||||
|
||||
/**
|
||||
* Wrapper for levendist.js to play nicely with Quickshell's imports
|
||||
*/
|
||||
|
||||
Singleton {
|
||||
function computeScore(...args) {
|
||||
return Levendist.computeScore(...args)
|
||||
}
|
||||
|
||||
function computeTextMatchScore(...args) {
|
||||
return Levendist.computeTextMatchScore(...args)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,111 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
/**
|
||||
* @param { string } summary
|
||||
* @returns { string }
|
||||
*/
|
||||
function findSuitableMaterialSymbol(summary = "") {
|
||||
const defaultType = 'chat';
|
||||
if (summary.length === 0) return defaultType;
|
||||
|
||||
const keywordsToTypes = {
|
||||
'reboot': 'restart_alt',
|
||||
'record': 'screen_record',
|
||||
'battery': 'power',
|
||||
'power': 'power',
|
||||
'screenshot': 'screenshot_monitor',
|
||||
'welcome': 'waving_hand',
|
||||
'time': 'scheduleb',
|
||||
'installed': 'download',
|
||||
'configuration reloaded': 'reset_wrench',
|
||||
'unable': 'question_mark',
|
||||
"couldn't": 'question_mark',
|
||||
'config': 'reset_wrench',
|
||||
'update': 'update',
|
||||
'ai response': 'neurology',
|
||||
'control': 'settings',
|
||||
'upsca': 'compare',
|
||||
'music': 'queue_music',
|
||||
'install': 'deployed_code_update',
|
||||
'input': 'keyboard_alt',
|
||||
'preedit': 'keyboard_alt',
|
||||
'startswith:file': 'folder_copy', // Declarative startsWith check
|
||||
};
|
||||
|
||||
const lowerSummary = summary.toLowerCase();
|
||||
|
||||
for (const [keyword, type] of Object.entries(keywordsToTypes)) {
|
||||
if (keyword.startsWith('startswith:')) {
|
||||
const startsWithKeyword = keyword.replace('startswith:', '');
|
||||
if (lowerSummary.startsWith(startsWithKeyword)) {
|
||||
return type;
|
||||
}
|
||||
} else if (lowerSummary.includes(keyword)) {
|
||||
return type;
|
||||
}
|
||||
}
|
||||
|
||||
return defaultType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param { number | string | Date } timestamp
|
||||
* @returns { string }
|
||||
*/
|
||||
function getFriendlyNotifTimeString(timestamp) {
|
||||
if (!timestamp) return '';
|
||||
const messageTime = new Date(timestamp);
|
||||
const now = new Date();
|
||||
const diffMs = now.getTime() - messageTime.getTime();
|
||||
|
||||
// Less than 1 minute
|
||||
if (diffMs < 60000)
|
||||
return 'Now';
|
||||
|
||||
// Same day - show relative time
|
||||
if (messageTime.toDateString() === now.toDateString()) {
|
||||
const diffMinutes = Math.floor(diffMs / 60000);
|
||||
const diffHours = Math.floor(diffMs / 3600000);
|
||||
|
||||
if (diffHours > 0) {
|
||||
return `${diffHours}h`;
|
||||
} else {
|
||||
return `${diffMinutes}m`;
|
||||
}
|
||||
}
|
||||
|
||||
// Yesterday
|
||||
if (messageTime.toDateString() === new Date(now.getTime() - 86400000).toDateString())
|
||||
return 'Yesterday';
|
||||
|
||||
// Older dates
|
||||
return Qt.formatDateTime(messageTime, "MMMM dd");
|
||||
}
|
||||
|
||||
function processNotificationBody(body, appName) {
|
||||
let processedBody = body
|
||||
|
||||
// Clean Chromium-based browsers notifications - remove first line
|
||||
if (appName) {
|
||||
const lowerApp = appName.toLowerCase()
|
||||
const chromiumBrowsers = [
|
||||
"brave", "chrome", "chromium", "vivaldi", "opera", "microsoft edge"
|
||||
]
|
||||
|
||||
if (chromiumBrowsers.some(name => lowerApp.includes(name))) {
|
||||
const lines = body.split('\n\n')
|
||||
|
||||
if (lines.length > 1 && lines[0].startsWith('<a')) {
|
||||
processedBody = lines.slice(1).join('\n\n')
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
processedBody = processedBody.replace(/<img/gi, '\n\n<img');
|
||||
|
||||
return processedBody
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function toPlainObject(qtObj) {
|
||||
if (qtObj === null || typeof qtObj !== "object") return qtObj;
|
||||
|
||||
// Handle true arrays
|
||||
if (Array.isArray(qtObj)) {
|
||||
return qtObj.map(item => toPlainObject(item));
|
||||
}
|
||||
|
||||
// Handle array-like Qt objects (e.g., have length and numeric keys)
|
||||
if (
|
||||
typeof qtObj.length === "number" &&
|
||||
qtObj.length > 0 &&
|
||||
Object.keys(qtObj).every(
|
||||
key => !isNaN(key) || key === "length"
|
||||
)
|
||||
) {
|
||||
let arr = [];
|
||||
for (let i = 0; i < qtObj.length; i++) {
|
||||
arr.push(toPlainObject(qtObj[i]));
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
const result = ({});
|
||||
for (let key in qtObj) {
|
||||
if (
|
||||
typeof qtObj[key] !== "function" &&
|
||||
!key.startsWith("objectName") &&
|
||||
!key.startsWith("children") &&
|
||||
!key.startsWith("object") &&
|
||||
!key.startsWith("parent") &&
|
||||
!key.startsWith("metaObject") &&
|
||||
!key.startsWith("destroyed") &&
|
||||
!key.startsWith("reloadableId")
|
||||
) {
|
||||
result[key] = toPlainObject(qtObj[key]);
|
||||
}
|
||||
}
|
||||
// console.log(JSON.stringify(result))
|
||||
return result;
|
||||
}
|
||||
|
||||
function applyToQtObject(qtObj, jsonObj) {
|
||||
// console.log("applyToQtObject", JSON.stringify(qtObj, null, 2), "<<", JSON.stringify(jsonObj, null, 2));
|
||||
if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return;
|
||||
|
||||
// Detect array-like Qt objects
|
||||
const isQtArrayLike = obj => {
|
||||
return obj && typeof obj === "object" &&
|
||||
typeof obj.length === "number" &&
|
||||
obj.length > 0 &&
|
||||
Object.keys(obj).every(key => !isNaN(key) || key === "length");
|
||||
};
|
||||
|
||||
// If both are arrays or array-like, update in place or replace
|
||||
if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) {
|
||||
qtObj.length = 0;
|
||||
for (let i = 0; i < jsonObj.length; i++) {
|
||||
qtObj.push(jsonObj[i]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// If target is array or array-like but source is not, clear
|
||||
if ((Array.isArray(qtObj) || isQtArrayLike(qtObj)) && !Array.isArray(jsonObj)) {
|
||||
qtObj.length = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
// If source is array but target is not, assign directly if possible
|
||||
if (!(Array.isArray(qtObj) || isQtArrayLike(qtObj)) && Array.isArray(jsonObj)) {
|
||||
return jsonObj;
|
||||
}
|
||||
|
||||
for (let key in jsonObj) {
|
||||
if (!qtObj.hasOwnProperty(key)) continue;
|
||||
const value = qtObj[key];
|
||||
const jsonValue = jsonObj[key];
|
||||
// console.log("applying to qt obj key:", value, "jsonValue:", jsonValue);
|
||||
if ((Array.isArray(value) || isQtArrayLike(value)) && Array.isArray(jsonValue)) {
|
||||
value.length = 0;
|
||||
for (let i = 0; i < jsonValue.length; i++) {
|
||||
value.push(jsonValue[i]);
|
||||
}
|
||||
} else if (value && typeof value === "object" && !Array.isArray(value) && !isQtArrayLike(value)) {
|
||||
applyToQtObject(value, jsonValue);
|
||||
} else {
|
||||
qtObj[key] = jsonValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
import Quickshell.Services.Mpris
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
function closeAllWindows() {
|
||||
HyprlandData.windowList.map(w => w.pid).forEach(pid => {
|
||||
Quickshell.execDetached(["kill", pid]);
|
||||
});
|
||||
}
|
||||
|
||||
function changePassword() {
|
||||
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.changePassword}`]);
|
||||
}
|
||||
|
||||
function lock() {
|
||||
Quickshell.execDetached(["loginctl", "lock-session"]);
|
||||
}
|
||||
|
||||
function suspend() {
|
||||
pauseAllPlayers();
|
||||
Quickshell.execDetached(["bash", "-c", "systemctl suspend || loginctl suspend"]);
|
||||
}
|
||||
|
||||
function logout() {
|
||||
closeAllWindows();
|
||||
Quickshell.execDetached(["pkill", "-i", "Hyprland"]);
|
||||
}
|
||||
|
||||
function launchTaskManager() {
|
||||
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.taskManager}`]);
|
||||
}
|
||||
|
||||
function hibernate() {
|
||||
pauseAllPlayers();
|
||||
Quickshell.execDetached(["bash", "-c", `systemctl hibernate || loginctl hibernate`]);
|
||||
}
|
||||
|
||||
function pauseAllPlayers() {
|
||||
for (const player of Mpris.players.values) {
|
||||
if (player.canPause) player.pause();
|
||||
}
|
||||
}
|
||||
|
||||
function poweroff() {
|
||||
closeAllWindows();
|
||||
Quickshell.execDetached(["bash", "-c", `systemctl poweroff || loginctl poweroff`]);
|
||||
}
|
||||
|
||||
function reboot() {
|
||||
closeAllWindows();
|
||||
Quickshell.execDetached(["bash", "-c", `reboot || loginctl reboot`]);
|
||||
}
|
||||
|
||||
function rebootToFirmware() {
|
||||
closeAllWindows();
|
||||
Quickshell.execDetached(["bash", "-c", `systemctl reboot --firmware-setup || loginctl reboot --firmware-setup`]);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,375 @@
|
|||
pragma Singleton
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
/**
|
||||
* Formats a string according to the args that are passed inc
|
||||
* @param { string } str
|
||||
* @param {...any} args
|
||||
* @returns { string }
|
||||
*/
|
||||
function format(str, ...args) {
|
||||
return str.replace(/{(\d+)}/g, (match, index) => typeof args[index] !== 'undefined' ? args[index] : match);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the domain of the passed in url or null
|
||||
* @param { string } url
|
||||
* @returns { string| null }
|
||||
*/
|
||||
function getDomain(url) {
|
||||
const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the base url of the passed in url or null
|
||||
* @param { string } url
|
||||
* @returns { string | null }
|
||||
*/
|
||||
function getBaseUrl(url) {
|
||||
const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/);
|
||||
return match ? match[1] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes single quotes in shell commands
|
||||
* @param { string } str
|
||||
* @returns { string }
|
||||
*/
|
||||
function shellSingleQuoteEscape(str) {
|
||||
return String(str)
|
||||
// .replace(/\\/g, '\\\\')
|
||||
.replace(/'/g, "'\\''");
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits markdown blocks into three different types: text, think, and code.
|
||||
* @param { string } markdown
|
||||
* @returns {Array<{type: "text" | "think" | "code", content: string, lang?: string, completed?: boolean}>}
|
||||
*/
|
||||
function splitMarkdownBlocks(markdown) {
|
||||
const regex = /```(\w+)?\n([\s\S]*?)```|<think>([\s\S]*?)<\/think>/g;
|
||||
/**
|
||||
* @type {{type: "text" | "think" | "code"; content: string; lang: string | undefined; completed: boolean | undefined}[]}
|
||||
*/
|
||||
let result = [];
|
||||
let lastIndex = 0;
|
||||
let match;
|
||||
while ((match = regex.exec(markdown)) !== null) {
|
||||
if (match.index > lastIndex) {
|
||||
const text = markdown.slice(lastIndex, match.index);
|
||||
if (text.trim()) {
|
||||
result.push({
|
||||
type: "text",
|
||||
content: text
|
||||
});
|
||||
}
|
||||
}
|
||||
if (match[0].startsWith('```')) {
|
||||
if (match[2] && match[2].trim()) {
|
||||
result.push({
|
||||
type: "code",
|
||||
lang: match[1] || "",
|
||||
content: match[2],
|
||||
completed: true
|
||||
});
|
||||
}
|
||||
} else if (match[0].startsWith('<think>')) {
|
||||
if (match[3] && match[3].trim()) {
|
||||
result.push({
|
||||
type: "think",
|
||||
content: match[3],
|
||||
completed: true
|
||||
});
|
||||
}
|
||||
}
|
||||
lastIndex = regex.lastIndex;
|
||||
}
|
||||
// Handle any remaining text after the last match
|
||||
if (lastIndex < markdown.length) {
|
||||
const text = markdown.slice(lastIndex);
|
||||
// Check for unfinished <think> block
|
||||
const thinkStart = text.indexOf('<think>');
|
||||
const codeStart = text.indexOf('```');
|
||||
if (thinkStart !== -1 && (codeStart === -1 || thinkStart < codeStart)) {
|
||||
const beforeThink = text.slice(0, thinkStart);
|
||||
if (beforeThink.trim()) {
|
||||
result.push({
|
||||
type: "text",
|
||||
content: beforeThink
|
||||
});
|
||||
}
|
||||
const thinkContent = text.slice(thinkStart + 7);
|
||||
if (thinkContent.trim()) {
|
||||
result.push({
|
||||
type: "think",
|
||||
content: thinkContent,
|
||||
completed: false
|
||||
});
|
||||
}
|
||||
} else if (codeStart !== -1) {
|
||||
const beforeCode = text.slice(0, codeStart);
|
||||
if (beforeCode.trim()) {
|
||||
result.push({
|
||||
type: "text",
|
||||
content: beforeCode
|
||||
});
|
||||
}
|
||||
// Try to detect language after ```
|
||||
const codeLangMatch = text.slice(codeStart + 3).match(/^(\w+)?\n/);
|
||||
let lang = "";
|
||||
let codeContentStart = codeStart + 3;
|
||||
if (codeLangMatch) {
|
||||
lang = codeLangMatch[1] || "";
|
||||
codeContentStart += codeLangMatch[0].length;
|
||||
} else if (text[codeStart + 3] === '\n') {
|
||||
codeContentStart += 1;
|
||||
}
|
||||
const codeContent = text.slice(codeContentStart);
|
||||
if (codeContent.trim()) {
|
||||
result.push({
|
||||
type: "code",
|
||||
lang,
|
||||
content: codeContent,
|
||||
completed: false
|
||||
});
|
||||
}
|
||||
} else if (text.trim()) {
|
||||
result.push({
|
||||
type: "text",
|
||||
content: text
|
||||
});
|
||||
}
|
||||
}
|
||||
// console.log(JSON.stringify(result, null, 2));
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the original string with backslashes escaped
|
||||
* @param { string } str
|
||||
* @returns { string }
|
||||
*/
|
||||
function escapeBackslashes(str) {
|
||||
return str.replace(/\\/g, '\\\\');
|
||||
}
|
||||
|
||||
// Pronunciation fixes for VibeVoice — word-boundary replacements so the
|
||||
// model says names and acronyms correctly. Ported from lettabot's Matrix
|
||||
// TTS (src/channels/matrix/tts.ts), tuned for this voice over time.
|
||||
readonly property var _ttsPronunciation: ({
|
||||
"Xzaviar": "X-zay-V-ar", "xzaviar": "X-zay-V-ar",
|
||||
"Jean Luc": "Zhan-Look", "jean luc": "Zhan-Look",
|
||||
"Sebastian": "Se-BASS-chen", "sebastian": "Se-BASS-chen",
|
||||
"API": "A P I", "SDK": "S D K",
|
||||
"E2EE": "end-to-end encrypted",
|
||||
"TTS": "text to speech", "STT": "speech to text",
|
||||
})
|
||||
|
||||
function _applyTtsPronunciation(text) {
|
||||
let result = text;
|
||||
for (const wrong in root._ttsPronunciation) {
|
||||
const right = root._ttsPronunciation[wrong];
|
||||
result = result.replace(new RegExp(`\\b${wrong.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi"), right);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans assistant reply text for text-to-speech: strips <think> blocks,
|
||||
* agent control tags, HTML, code fences, markdown, and most emoji, and
|
||||
* applies pronunciation fixes — so only the spoken voice remains.
|
||||
* Returns the cleaned text (empty string if nothing speakable survives).
|
||||
* Order (ported from lettabot's cleanTextForTTS, plus Souveraine think
|
||||
* blocks): control tags → color/spoiler → HTML → think → code/markdown →
|
||||
* emoji → pronunciation → whitespace.
|
||||
* @param { string } text
|
||||
* @returns { string }
|
||||
*/
|
||||
function ttsClean(text) {
|
||||
if (!text)
|
||||
return "";
|
||||
let cleaned = text;
|
||||
// Strip agent control tags
|
||||
cleaned = cleaned.replace(/\[silent\]/gi, "");
|
||||
cleaned = cleaned.replace(/\[chromatophore\]/gi, "");
|
||||
cleaned = cleaned.replace(/\[!c\]/gi, "");
|
||||
cleaned = cleaned.replace(/\[!s\]/gi, "");
|
||||
cleaned = cleaned.replace(/\[react:[^\]]*\]/gi, "");
|
||||
// Strip color syntax {color|text} → keep text; spoilers ||text|| → keep text
|
||||
cleaned = cleaned.replace(/\{[^}|]+\|([^}]+)\}/g, "$1");
|
||||
// dotAll via [\s\S] (not the 's' flag — older Qt/V8 rejects it)
|
||||
cleaned = cleaned.replace(/\|\|([\s\S]+?)\|\|/g, "$1");
|
||||
// Remove <think>...</think> blocks entirely (Souveraine reasoning).
|
||||
// MUST run before the HTML-tag strip, or the tags get eaten first and
|
||||
// the reasoning survives as plain text. Also drop an unclosed <think>
|
||||
// (stream cut mid-reasoning) so we never speak a half-thought — by
|
||||
// default we want only the actual message, not the thinking.
|
||||
cleaned = cleaned.replace(/<think>[\s\S]*?<\/think>/g, " ");
|
||||
cleaned = cleaned.replace(/<think>[\s\S]*$/g, " ");
|
||||
// Strip HTML tags (keep content)
|
||||
cleaned = cleaned.replace(/<[^>]+>/g, "");
|
||||
// Remove markdown headers, bold, italic, code fences
|
||||
cleaned = cleaned.replace(/```[\s\S]*?```/g, " ");
|
||||
cleaned = cleaned.replace(/#{1,6}\s/g, " ");
|
||||
cleaned = cleaned.replace(/\*\*([^*]+)\*\*/g, "$1");
|
||||
cleaned = cleaned.replace(/\*([^*]+)\*/g, "$1");
|
||||
cleaned = cleaned.replace(/`([^`]+)`/g, "$1");
|
||||
// Remove markdown links and images
|
||||
cleaned = cleaned.replace(/!\[([^\]]*)\]\([^)]+\)/g, "$1");
|
||||
cleaned = cleaned.replace(/\[([^\]]*)\]\([^)]+\)/g, "$1");
|
||||
// Emoji: preserve ✨ and 🎤 (the model says those fine), strip the rest.
|
||||
// Marker-swap so the preserved ones survive the broad Unicode strip.
|
||||
const sparkle = cleaned.includes("✨"), mic = cleaned.includes("🎤");
|
||||
cleaned = cleaned.replace(/✨/g, "SPK").replace(/🎤/g, "MIC");
|
||||
cleaned = cleaned.replace(/[\u{1F600}-\u{1F64F}\u{1F300}-\u{1F5FF}\u{1F680}-\u{1F6FF}\u{1F1E0}-\u{1F1FF}\u{2702}-\u{27B0}\u{24C2}-\u{1F251}\u{1F900}-\u{1FAFF}]/gu, "");
|
||||
cleaned = cleaned.replace(/SPK/g, sparkle ? "✨" : "").replace(/MIC/g, mic ? "🎤" : "");
|
||||
// Pronunciation fixes
|
||||
cleaned = root._applyTtsPronunciation(cleaned);
|
||||
// Collapse whitespace
|
||||
cleaned = cleaned.replace(/\s+/g, " ").trim();
|
||||
return cleaned;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps words to supplied maximum length
|
||||
* @param { string | null } str
|
||||
* @param { number } maxLen
|
||||
* @returns { string }
|
||||
*/
|
||||
function wordWrap(str, maxLen) {
|
||||
if (!str)
|
||||
return "";
|
||||
let words = str.split(" ");
|
||||
let lines = [];
|
||||
let current = "";
|
||||
for (let i = 0; i < words.length; ++i) {
|
||||
if ((current + (current.length > 0 ? " " : "") + words[i]).length > maxLen) {
|
||||
if (current.length > 0)
|
||||
lines.push(current);
|
||||
current = words[i];
|
||||
} else {
|
||||
current += (current.length > 0 ? " " : "") + words[i];
|
||||
}
|
||||
}
|
||||
if (current.length > 0)
|
||||
lines.push(current);
|
||||
return lines.join("\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up a music title by removing bracketed and special characters.
|
||||
* @param { string } title
|
||||
* @returns { string }
|
||||
*/
|
||||
function cleanMusicTitle(title) {
|
||||
if (!title)
|
||||
return "";
|
||||
// Brackets
|
||||
title = title.replace(/^ *\([^)]*\) */g, " "); // Round brackets
|
||||
title = title.replace(/^ *\[[^\]]*\] */g, " "); // Square brackets
|
||||
title = title.replace(/^ *\{[^\}]*\} */g, " "); // Curly brackets
|
||||
// Japenis brackets
|
||||
title = title.replace(/^ *【[^】]*】/, ""); // Touhou
|
||||
title = title.replace(/^ *《[^》]*》/, ""); // ??
|
||||
title = title.replace(/^ *「[^」]*」/, ""); // OP/ED thingie
|
||||
title = title.replace(/^ *『[^』]*』/, ""); // OP/ED thingie
|
||||
|
||||
return title.trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts seconds to a friendly time string (e.g. 1:23 or 1:02:03).
|
||||
* @param { number } seconds
|
||||
* @returns { string }
|
||||
*/
|
||||
function friendlyTimeForSeconds(seconds) {
|
||||
if (isNaN(seconds) || seconds < 0)
|
||||
return "0:00";
|
||||
seconds = Math.floor(seconds);
|
||||
const h = Math.floor(seconds / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
const s = seconds % 60;
|
||||
if (h > 0) {
|
||||
return `${h}:${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')}`;
|
||||
} else {
|
||||
return `${m}:${s.toString().padStart(2, '0')}`;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes HTML special characters in a string.
|
||||
* @param { string } str
|
||||
* @returns { string }
|
||||
*/
|
||||
function escapeHtml(str) {
|
||||
if (typeof str !== 'string')
|
||||
return str;
|
||||
return str.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans a cliphist entry by removing leading digits and tab.
|
||||
* @param { string } str
|
||||
* @returns { string }
|
||||
*/
|
||||
function cleanCliphistEntry(str: string): string {
|
||||
return str.replace(/^\d+\t/, "");
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if any substring in the list is contained in the string.
|
||||
* @param { string } str
|
||||
* @param { string[] } substrings
|
||||
* @returns { boolean }
|
||||
*/
|
||||
function stringListContainsSubstring(str, substrings) {
|
||||
for (let i = 0; i < substrings.length; ++i) {
|
||||
if (str.includes(substrings[i])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the given prefix from the string if present.
|
||||
* @param { string } str
|
||||
* @param { string } prefix
|
||||
* @returns { string }
|
||||
*/
|
||||
function cleanPrefix(str, prefix) {
|
||||
if (str.startsWith(prefix)) {
|
||||
return str.slice(prefix.length);
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes the first matching prefix from the string if present.
|
||||
* @param { string } str
|
||||
* @param { string[] } prefixes
|
||||
* @returns { string }
|
||||
*/
|
||||
function cleanOnePrefix(str, prefixes) {
|
||||
for (let i = 0; i < prefixes.length; ++i) {
|
||||
if (str.startsWith(prefixes[i])) {
|
||||
return str.slice(prefixes[i].length);
|
||||
}
|
||||
}
|
||||
return str;
|
||||
}
|
||||
|
||||
function toTitleCase(str) {
|
||||
// Replace "-" and "_" with space, then capitalize each word
|
||||
return str.replace(/[-_]/g, " ").replace(
|
||||
/\w\S*/g,
|
||||
function(txt) {
|
||||
return txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase();
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,682 @@
|
|||
.pragma library
|
||||
|
||||
// https://github.com/farzher/fuzzysort
|
||||
// License: MIT | Copyright (c) 2018 Stephen Kamenar
|
||||
// A copy of the license is available in the `licenses` folder of this repository
|
||||
|
||||
var single = (search, target) => {
|
||||
if(!search || !target) return NULL
|
||||
|
||||
var preparedSearch = getPreparedSearch(search)
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
var searchBitflags = preparedSearch.bitflags
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) return NULL
|
||||
|
||||
return algorithm(preparedSearch, target)
|
||||
}
|
||||
|
||||
var go = (search, targets, options) => {
|
||||
if(!search) return options?.all ? all(targets, options) : noResults
|
||||
|
||||
var preparedSearch = getPreparedSearch(search)
|
||||
var searchBitflags = preparedSearch.bitflags
|
||||
var containsSpace = preparedSearch.containsSpace
|
||||
|
||||
var threshold = denormalizeScore( options?.threshold || 0 )
|
||||
var limit = options?.limit || INFINITY
|
||||
|
||||
var resultsLen = 0; var limitedCount = 0
|
||||
var targetsLen = targets.length
|
||||
|
||||
function push_result(result) {
|
||||
if(resultsLen < limit) { q.add(result); ++resultsLen }
|
||||
else {
|
||||
++limitedCount
|
||||
if(result._score > q.peek()._score) q.replaceTop(result)
|
||||
}
|
||||
}
|
||||
|
||||
// This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys]
|
||||
|
||||
// options.key
|
||||
if(options?.key) {
|
||||
var key = options.key
|
||||
for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
||||
var target = getValue(obj, key)
|
||||
if(!target) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
||||
var result = algorithm(preparedSearch, target)
|
||||
if(result === NULL) continue
|
||||
if(result._score < threshold) continue
|
||||
|
||||
result.obj = obj
|
||||
push_result(result)
|
||||
}
|
||||
|
||||
// options.keys
|
||||
} else if(options?.keys) {
|
||||
var keys = options.keys
|
||||
var keysLen = keys.length
|
||||
|
||||
outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i]
|
||||
|
||||
{ // early out based on bitflags
|
||||
var keysBitflags = 0
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
var key = keys[keyI]
|
||||
var target = getValue(obj, key)
|
||||
if(!target) { tmpTargets[keyI] = noTarget; continue }
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
tmpTargets[keyI] = target
|
||||
|
||||
keysBitflags |= target._bitflags
|
||||
}
|
||||
|
||||
if((searchBitflags & keysBitflags) !== searchBitflags) continue
|
||||
}
|
||||
|
||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY
|
||||
|
||||
for (var keyI = 0; keyI < keysLen; ++keyI) {
|
||||
target = tmpTargets[keyI]
|
||||
if(target === noTarget) { tmpResults[keyI] = noTarget; continue }
|
||||
|
||||
tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace)
|
||||
if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue }
|
||||
|
||||
// todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it
|
||||
// if our second match isn't good we ignore it instead of averaging with it
|
||||
if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) {
|
||||
if(allowPartialMatchScores[i] > -1000) {
|
||||
if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) {
|
||||
var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/
|
||||
if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp
|
||||
}
|
||||
}
|
||||
if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i]
|
||||
}
|
||||
}
|
||||
|
||||
if(containsSpace) {
|
||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer }
|
||||
} else {
|
||||
var hasAtLeast1Match = false
|
||||
for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } }
|
||||
if(!hasAtLeast1Match) continue
|
||||
}
|
||||
|
||||
var objResults = new KeysResult(keysLen)
|
||||
for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] }
|
||||
|
||||
if(containsSpace) {
|
||||
var score = 0
|
||||
for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i]
|
||||
} else {
|
||||
// todo could rewrite this scoring to be more similar to when there's spaces
|
||||
// if we match multiple keys give us bonus points
|
||||
var score = NEGATIVE_INFINITY
|
||||
for(let i=0; i<keysLen; i++) {
|
||||
var result = objResults[i]
|
||||
if(result._score > -1000) {
|
||||
if(score > NEGATIVE_INFINITY) {
|
||||
var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/
|
||||
if(tmp > score) score = tmp
|
||||
}
|
||||
}
|
||||
if(result._score > score) score = result._score
|
||||
}
|
||||
}
|
||||
|
||||
objResults.obj = obj
|
||||
objResults._score = score
|
||||
if(options?.scoreFn) {
|
||||
score = options.scoreFn(objResults)
|
||||
if(!score) continue
|
||||
score = denormalizeScore(score)
|
||||
objResults._score = score
|
||||
}
|
||||
|
||||
if(score < threshold) continue
|
||||
push_result(objResults)
|
||||
}
|
||||
|
||||
// no keys
|
||||
} else {
|
||||
for(var i = 0; i < targetsLen; ++i) { var target = targets[i]
|
||||
if(!target) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
|
||||
if((searchBitflags & target._bitflags) !== searchBitflags) continue
|
||||
var result = algorithm(preparedSearch, target)
|
||||
if(result === NULL) continue
|
||||
if(result._score < threshold) continue
|
||||
|
||||
push_result(result)
|
||||
}
|
||||
}
|
||||
|
||||
if(resultsLen === 0) return noResults
|
||||
var results = new Array(resultsLen)
|
||||
for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll()
|
||||
results.total = resultsLen + limitedCount
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
// this is written as 1 function instead of 2 for minification. perf seems fine ...
|
||||
// except when minified. the perf is very slow
|
||||
var highlight = (result, open='<b>', close='</b>') => {
|
||||
var callback = typeof open === 'function' ? open : undefined
|
||||
|
||||
var target = result.target
|
||||
var targetLen = target.length
|
||||
var indexes = result.indexes
|
||||
var highlighted = ''
|
||||
var matchI = 0
|
||||
var indexesI = 0
|
||||
var opened = false
|
||||
var parts = []
|
||||
|
||||
for(var i = 0; i < targetLen; ++i) { var char = target[i]
|
||||
if(indexes[indexesI] === i) {
|
||||
++indexesI
|
||||
if(!opened) { opened = true
|
||||
if(callback) {
|
||||
parts.push(highlighted); highlighted = ''
|
||||
} else {
|
||||
highlighted += open
|
||||
}
|
||||
}
|
||||
|
||||
if(indexesI === indexes.length) {
|
||||
if(callback) {
|
||||
highlighted += char
|
||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
||||
parts.push(target.substr(i+1))
|
||||
} else {
|
||||
highlighted += char + close + target.substr(i+1)
|
||||
}
|
||||
break
|
||||
}
|
||||
} else {
|
||||
if(opened) { opened = false
|
||||
if(callback) {
|
||||
parts.push(callback(highlighted, matchI++)); highlighted = ''
|
||||
} else {
|
||||
highlighted += close
|
||||
}
|
||||
}
|
||||
}
|
||||
highlighted += char
|
||||
}
|
||||
|
||||
return callback ? parts : highlighted
|
||||
}
|
||||
|
||||
|
||||
var prepare = (target) => {
|
||||
if(typeof target === 'number') target = ''+target
|
||||
else if(typeof target !== 'string') target = ''
|
||||
var info = prepareLowerInfo(target)
|
||||
return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags})
|
||||
}
|
||||
|
||||
var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() }
|
||||
|
||||
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
// Below this point is only internal code
|
||||
|
||||
|
||||
class Result {
|
||||
get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) }
|
||||
set ['indexes'](indexes) { return this._indexes = indexes }
|
||||
['highlight'](open, close) { return highlight(this, open, close) }
|
||||
get ['score']() { return normalizeScore(this._score) }
|
||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
||||
}
|
||||
|
||||
class KeysResult extends Array {
|
||||
get ['score']() { return normalizeScore(this._score) }
|
||||
set ['score'](score) { this._score = denormalizeScore(score) }
|
||||
}
|
||||
|
||||
var new_result = (target, options) => {
|
||||
const result = new Result()
|
||||
result['target'] = target
|
||||
result['obj'] = options.obj ?? NULL
|
||||
result._score = options._score ?? NEGATIVE_INFINITY
|
||||
result._indexes = options._indexes ?? []
|
||||
result._targetLower = options._targetLower ?? ''
|
||||
result._targetLowerCodes = options._targetLowerCodes ?? NULL
|
||||
result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL
|
||||
result._bitflags = options._bitflags ?? 0
|
||||
return result
|
||||
}
|
||||
|
||||
|
||||
var normalizeScore = score => {
|
||||
if(score === NEGATIVE_INFINITY) return 0
|
||||
if(score > 1) return score
|
||||
return Math.E ** ( ((-score + 1)**.04307 - 1) * -2)
|
||||
}
|
||||
var denormalizeScore = normalizedScore => {
|
||||
if(normalizedScore === 0) return NEGATIVE_INFINITY
|
||||
if(normalizedScore > 1) return normalizedScore
|
||||
return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307)
|
||||
}
|
||||
|
||||
|
||||
var prepareSearch = (search) => {
|
||||
if(typeof search === 'number') search = ''+search
|
||||
else if(typeof search !== 'string') search = ''
|
||||
search = search.trim()
|
||||
var info = prepareLowerInfo(search)
|
||||
|
||||
var spaceSearches = []
|
||||
if(info.containsSpace) {
|
||||
var searches = search.split(/\s+/)
|
||||
searches = [...new Set(searches)] // distinct
|
||||
for(var i=0; i<searches.length; i++) {
|
||||
if(searches[i] === '') continue
|
||||
var _info = prepareLowerInfo(searches[i])
|
||||
spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false})
|
||||
}
|
||||
}
|
||||
|
||||
return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches}
|
||||
}
|
||||
|
||||
|
||||
|
||||
var getPrepared = (target) => {
|
||||
if(target.length > 999) return prepare(target) // don't cache huge targets
|
||||
var targetPrepared = preparedCache.get(target)
|
||||
if(targetPrepared !== undefined) return targetPrepared
|
||||
targetPrepared = prepare(target)
|
||||
preparedCache.set(target, targetPrepared)
|
||||
return targetPrepared
|
||||
}
|
||||
var getPreparedSearch = (search) => {
|
||||
if(search.length > 999) return prepareSearch(search) // don't cache huge searches
|
||||
var searchPrepared = preparedSearchCache.get(search)
|
||||
if(searchPrepared !== undefined) return searchPrepared
|
||||
searchPrepared = prepareSearch(search)
|
||||
preparedSearchCache.set(search, searchPrepared)
|
||||
return searchPrepared
|
||||
}
|
||||
|
||||
|
||||
var all = (targets, options) => {
|
||||
var results = []; results.total = targets.length // this total can be wrong if some targets are skipped
|
||||
|
||||
var limit = options?.limit || INFINITY
|
||||
|
||||
if(options?.key) {
|
||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
||||
var target = getValue(obj, options.key)
|
||||
if(target == NULL) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
var result = new_result(target.target, {_score: target._score, obj: obj})
|
||||
results.push(result); if(results.length >= limit) return results
|
||||
}
|
||||
} else if(options?.keys) {
|
||||
for(var i=0;i<targets.length;i++) { var obj = targets[i]
|
||||
var objResults = new KeysResult(options.keys.length)
|
||||
for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) {
|
||||
var target = getValue(obj, options.keys[keyI])
|
||||
if(!target) { objResults[keyI] = noTarget; continue }
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
target._score = NEGATIVE_INFINITY
|
||||
target._indexes.len = 0
|
||||
objResults[keyI] = target
|
||||
}
|
||||
objResults.obj = obj
|
||||
objResults._score = NEGATIVE_INFINITY
|
||||
results.push(objResults); if(results.length >= limit) return results
|
||||
}
|
||||
} else {
|
||||
for(var i=0;i<targets.length;i++) { var target = targets[i]
|
||||
if(target == NULL) continue
|
||||
if(!isPrepared(target)) target = getPrepared(target)
|
||||
target._score = NEGATIVE_INFINITY
|
||||
target._indexes.len = 0
|
||||
results.push(target); if(results.length >= limit) return results
|
||||
}
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
|
||||
var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => {
|
||||
if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch)
|
||||
|
||||
var searchLower = preparedSearch._lower
|
||||
var searchLowerCodes = preparedSearch.lowerCodes
|
||||
var searchLowerCode = searchLowerCodes[0]
|
||||
var targetLowerCodes = prepared._targetLowerCodes
|
||||
var searchLen = searchLowerCodes.length
|
||||
var targetLen = targetLowerCodes.length
|
||||
var searchI = 0 // where we at
|
||||
var targetI = 0 // where you at
|
||||
var matchesSimpleLen = 0
|
||||
|
||||
// very basic fuzzy match; to remove non-matching targets ASAP!
|
||||
// walk through target. find sequential matches.
|
||||
// if all chars aren't found then exit
|
||||
for(;;) {
|
||||
var isMatch = searchLowerCode === targetLowerCodes[targetI]
|
||||
if(isMatch) {
|
||||
matchesSimple[matchesSimpleLen++] = targetI
|
||||
++searchI; if(searchI === searchLen) break
|
||||
searchLowerCode = searchLowerCodes[searchI]
|
||||
}
|
||||
++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI
|
||||
}
|
||||
|
||||
var searchI = 0
|
||||
var successStrict = false
|
||||
var matchesStrictLen = 0
|
||||
|
||||
var nextBeginningIndexes = prepared._nextBeginningIndexes
|
||||
if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target)
|
||||
targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1]
|
||||
|
||||
// Our target string successfully matched all characters in sequence!
|
||||
// Let's try a more advanced and strict test to improve the score
|
||||
// only count it as a match if it's consecutive or a beginning character!
|
||||
var backtrackCount = 0
|
||||
if(targetI !== targetLen) for(;;) {
|
||||
if(targetI >= targetLen) {
|
||||
// We failed to find a good spot for this search char, go back to the previous search char and force it forward
|
||||
if(searchI <= 0) break // We failed to push chars forward for a better match
|
||||
|
||||
++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match
|
||||
|
||||
--searchI
|
||||
var lastMatch = matchesStrict[--matchesStrictLen]
|
||||
targetI = nextBeginningIndexes[lastMatch]
|
||||
|
||||
} else {
|
||||
var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI]
|
||||
if(isMatch) {
|
||||
matchesStrict[matchesStrictLen++] = targetI
|
||||
++searchI; if(searchI === searchLen) { successStrict = true; break }
|
||||
++targetI
|
||||
} else {
|
||||
targetI = nextBeginningIndexes[targetI]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// check if it's a substring match
|
||||
var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow
|
||||
var isSubstring = !!~substringIndex
|
||||
var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex
|
||||
|
||||
// if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score
|
||||
if(isSubstring && !isSubstringBeginning) {
|
||||
for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) {
|
||||
if(i <= substringIndex) continue
|
||||
|
||||
for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break
|
||||
if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break }
|
||||
}
|
||||
}
|
||||
|
||||
// tally up the score & keep track of matches for highlighting later
|
||||
// if it's a simple match, we'll switch to a substring match if a substring exists
|
||||
// if it's a strict match, we'll switch to a substring match only if that's a better score
|
||||
|
||||
var calculateScore = matches => {
|
||||
var score = 0
|
||||
|
||||
var extraMatchGroupCount = 0
|
||||
for(var i = 1; i < searchLen; ++i) {
|
||||
if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount}
|
||||
}
|
||||
var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1)
|
||||
|
||||
score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups
|
||||
|
||||
if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning
|
||||
|
||||
if(!successStrict) {
|
||||
score *= 1000
|
||||
} else {
|
||||
// successStrict on a target with too many beginning indexes loses points for being a bad target
|
||||
var uniqueBeginningIndexes = 1
|
||||
for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes
|
||||
|
||||
if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ...
|
||||
}
|
||||
|
||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
||||
|
||||
if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring
|
||||
if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex
|
||||
|
||||
score -= (targetLen - searchLen)/2 // penality for longer targets
|
||||
|
||||
return score
|
||||
}
|
||||
|
||||
if(!successStrict) {
|
||||
if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
||||
var matchesBest = matchesSimple
|
||||
var score = calculateScore(matchesBest)
|
||||
} else {
|
||||
if(isSubstringBeginning) {
|
||||
for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches
|
||||
var matchesBest = matchesSimple
|
||||
var score = calculateScore(matchesSimple)
|
||||
} else {
|
||||
var matchesBest = matchesStrict
|
||||
var score = calculateScore(matchesStrict)
|
||||
}
|
||||
}
|
||||
|
||||
prepared._score = score
|
||||
|
||||
for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i]
|
||||
prepared._indexes.len = searchLen
|
||||
|
||||
const result = new Result()
|
||||
result.target = prepared.target
|
||||
result._score = prepared._score
|
||||
result._indexes = prepared._indexes
|
||||
return result
|
||||
}
|
||||
var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => {
|
||||
var seen_indexes = new Set()
|
||||
var score = 0
|
||||
var result = NULL
|
||||
|
||||
var first_seen_index_last_search = 0
|
||||
var searches = preparedSearch.spaceSearches
|
||||
var searchesLen = searches.length
|
||||
var changeslen = 0
|
||||
|
||||
// Return _nextBeginningIndexes back to its normal state
|
||||
var resetNextBeginningIndexes = () => {
|
||||
for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1]
|
||||
}
|
||||
|
||||
var hasAtLeast1Match = false
|
||||
for(var i=0; i<searchesLen; ++i) {
|
||||
allowPartialMatchScores[i] = NEGATIVE_INFINITY
|
||||
var search = searches[i]
|
||||
|
||||
result = algorithm(search, target)
|
||||
if(allowPartialMatch) {
|
||||
if(result === NULL) continue
|
||||
hasAtLeast1Match = true
|
||||
} else {
|
||||
if(result === NULL) {resetNextBeginningIndexes(); return NULL}
|
||||
}
|
||||
|
||||
// if not the last search, we need to mutate _nextBeginningIndexes for the next search
|
||||
var isTheLastSearch = i === searchesLen - 1
|
||||
if(!isTheLastSearch) {
|
||||
var indexes = result._indexes
|
||||
|
||||
var indexesIsConsecutiveSubstring = true
|
||||
for(let i=0; i<indexes.len-1; i++) {
|
||||
if(indexes[i+1] - indexes[i] !== 1) {
|
||||
indexesIsConsecutiveSubstring = false; break;
|
||||
}
|
||||
}
|
||||
|
||||
if(indexesIsConsecutiveSubstring) {
|
||||
var newBeginningIndex = indexes[indexes.len-1] + 1
|
||||
var toReplace = target._nextBeginningIndexes[newBeginningIndex-1]
|
||||
for(let i=newBeginningIndex-1; i>=0; i--) {
|
||||
if(toReplace !== target._nextBeginningIndexes[i]) break
|
||||
target._nextBeginningIndexes[i] = newBeginningIndex
|
||||
nextBeginningIndexesChanges[changeslen*2 + 0] = i
|
||||
nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace
|
||||
changeslen++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
score += result._score / searchesLen
|
||||
allowPartialMatchScores[i] = result._score / searchesLen
|
||||
|
||||
// dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h
|
||||
if(result._indexes[0] < first_seen_index_last_search) {
|
||||
score -= (first_seen_index_last_search - result._indexes[0]) * 2
|
||||
}
|
||||
first_seen_index_last_search = result._indexes[0]
|
||||
|
||||
for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j])
|
||||
}
|
||||
|
||||
if(allowPartialMatch && !hasAtLeast1Match) return NULL
|
||||
|
||||
resetNextBeginningIndexes()
|
||||
|
||||
// allows a search with spaces that's an exact substring to score well
|
||||
var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true)
|
||||
if(allowSpacesResult !== NULL && allowSpacesResult._score > score) {
|
||||
if(allowPartialMatch) {
|
||||
for(var i=0; i<searchesLen; ++i) {
|
||||
allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen
|
||||
}
|
||||
}
|
||||
return allowSpacesResult
|
||||
}
|
||||
|
||||
if(allowPartialMatch) result = target
|
||||
result._score = score
|
||||
|
||||
var i = 0
|
||||
for (let index of seen_indexes) result._indexes[i++] = index
|
||||
result._indexes.len = i
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters
|
||||
var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '')
|
||||
|
||||
var prepareLowerInfo = (str) => {
|
||||
str = remove_accents(str)
|
||||
var strLen = str.length
|
||||
var lower = str.toLowerCase()
|
||||
var lowerCodes = [] // new Array(strLen) sparse array is too slow
|
||||
var bitflags = 0
|
||||
var containsSpace = false // space isn't stored in bitflags because of how searching with a space works
|
||||
|
||||
for(var i = 0; i < strLen; ++i) {
|
||||
var lowerCode = lowerCodes[i] = lower.charCodeAt(i)
|
||||
|
||||
if(lowerCode === 32) {
|
||||
containsSpace = true
|
||||
continue // it's important that we don't set any bitflags for space
|
||||
}
|
||||
|
||||
var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet
|
||||
: lowerCode>=48&&lowerCode<=57 ? 26 // numbers
|
||||
// 3 bits available
|
||||
: lowerCode<=127 ? 30 // other ascii
|
||||
: 31 // other utf8
|
||||
bitflags |= 1<<bit
|
||||
}
|
||||
|
||||
return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower}
|
||||
}
|
||||
var prepareBeginningIndexes = (target) => {
|
||||
var targetLen = target.length
|
||||
var beginningIndexes = []; var beginningIndexesLen = 0
|
||||
var wasUpper = false
|
||||
var wasAlphanum = false
|
||||
for(var i = 0; i < targetLen; ++i) {
|
||||
var targetCode = target.charCodeAt(i)
|
||||
var isUpper = targetCode>=65&&targetCode<=90
|
||||
var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57
|
||||
var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum
|
||||
wasUpper = isUpper
|
||||
wasAlphanum = isAlphanum
|
||||
if(isBeginning) beginningIndexes[beginningIndexesLen++] = i
|
||||
}
|
||||
return beginningIndexes
|
||||
}
|
||||
var prepareNextBeginningIndexes = (target) => {
|
||||
target = remove_accents(target)
|
||||
var targetLen = target.length
|
||||
var beginningIndexes = prepareBeginningIndexes(target)
|
||||
var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow
|
||||
var lastIsBeginning = beginningIndexes[0]
|
||||
var lastIsBeginningI = 0
|
||||
for(var i = 0; i < targetLen; ++i) {
|
||||
if(lastIsBeginning > i) {
|
||||
nextBeginningIndexes[i] = lastIsBeginning
|
||||
} else {
|
||||
lastIsBeginning = beginningIndexes[++lastIsBeginningI]
|
||||
nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning
|
||||
}
|
||||
}
|
||||
return nextBeginningIndexes
|
||||
}
|
||||
|
||||
var preparedCache = new Map()
|
||||
var preparedSearchCache = new Map()
|
||||
|
||||
// the theory behind these being globals is to reduce garbage collection by not making new arrays
|
||||
var matchesSimple = []; var matchesStrict = []
|
||||
var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search
|
||||
var keysSpacesBestScores = []; var allowPartialMatchScores = []
|
||||
var tmpTargets = []; var tmpResults = []
|
||||
|
||||
// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop]
|
||||
// prop = 'key1.key2' 10ms
|
||||
// prop = ['key1', 'key2'] 27ms
|
||||
// prop = obj => obj.tags.join() ??ms
|
||||
var getValue = (obj, prop) => {
|
||||
var tmp = obj[prop]; if(tmp !== undefined) return tmp
|
||||
if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower
|
||||
var segs = prop
|
||||
if(!Array.isArray(prop)) segs = prop.split('.')
|
||||
var len = segs.length
|
||||
var i = -1
|
||||
while (obj && (++i < len)) obj = obj[segs[i]]
|
||||
return obj
|
||||
}
|
||||
|
||||
var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' }
|
||||
var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY
|
||||
var noResults = []; noResults.total = 0
|
||||
var NULL = null
|
||||
|
||||
var noTarget = prepare('')
|
||||
|
||||
// Hacked version of https://github.com/lemire/FastPriorityQueue.js
|
||||
var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a}
|
||||
var q = fastpriorityqueue() // reuse this
|
||||
|
|
@ -0,0 +1,141 @@
|
|||
// Original code from https://github.com/koeqaife/hyprland-material-you
|
||||
// Original code license: GPLv3
|
||||
// Translated to Js from Cython with an LLM and reviewed
|
||||
|
||||
function min3(a, b, c) {
|
||||
return a < b && a < c ? a : b < c ? b : c;
|
||||
}
|
||||
|
||||
function max3(a, b, c) {
|
||||
return a > b && a > c ? a : b > c ? b : c;
|
||||
}
|
||||
|
||||
function min2(a, b) {
|
||||
return a < b ? a : b;
|
||||
}
|
||||
|
||||
function max2(a, b) {
|
||||
return a > b ? a : b;
|
||||
}
|
||||
|
||||
function levenshteinDistance(s1, s2) {
|
||||
let len1 = s1.length;
|
||||
let len2 = s2.length;
|
||||
|
||||
if (len1 === 0) return len2;
|
||||
if (len2 === 0) return len1;
|
||||
|
||||
if (len2 > len1) {
|
||||
[s1, s2] = [s2, s1];
|
||||
[len1, len2] = [len2, len1];
|
||||
}
|
||||
|
||||
let prev = new Array(len2 + 1);
|
||||
let curr = new Array(len2 + 1);
|
||||
|
||||
for (let j = 0; j <= len2; j++) {
|
||||
prev[j] = j;
|
||||
}
|
||||
|
||||
for (let i = 1; i <= len1; i++) {
|
||||
curr[0] = i;
|
||||
for (let j = 1; j <= len2; j++) {
|
||||
let cost = s1[i - 1] === s2[j - 1] ? 0 : 1;
|
||||
curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost);
|
||||
}
|
||||
[prev, curr] = [curr, prev];
|
||||
}
|
||||
|
||||
return prev[len2];
|
||||
}
|
||||
|
||||
function partialRatio(shortS, longS) {
|
||||
let lenS = shortS.length;
|
||||
let lenL = longS.length;
|
||||
let best = 0.0;
|
||||
|
||||
if (lenS === 0) return 1.0;
|
||||
|
||||
for (let i = 0; i <= lenL - lenS; i++) {
|
||||
let sub = longS.slice(i, i + lenS);
|
||||
let dist = levenshteinDistance(shortS, sub);
|
||||
let score = 1.0 - (dist / lenS);
|
||||
if (score > best) best = score;
|
||||
}
|
||||
|
||||
return best;
|
||||
}
|
||||
|
||||
function computeScore(s1, s2) {
|
||||
if (s1 === s2) return 1.0;
|
||||
|
||||
let dist = levenshteinDistance(s1, s2);
|
||||
let maxLen = max2(s1.length, s2.length);
|
||||
if (maxLen === 0) return 1.0;
|
||||
|
||||
let full = 1.0 - (dist / maxLen);
|
||||
let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
|
||||
|
||||
let score = 0.85 * full + 0.15 * part;
|
||||
|
||||
if (s1 && s2 && s1[0] !== s2[0]) {
|
||||
score -= 0.05;
|
||||
}
|
||||
|
||||
let lenDiff = Math.abs(s1.length - s2.length);
|
||||
if (lenDiff >= 3) {
|
||||
score -= 0.05 * lenDiff / maxLen;
|
||||
}
|
||||
|
||||
let commonPrefixLen = 0;
|
||||
let minLen = min2(s1.length, s2.length);
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
if (s1[i] === s2[i]) {
|
||||
commonPrefixLen++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
score += 0.02 * commonPrefixLen;
|
||||
|
||||
if (s1.includes(s2) || s2.includes(s1)) {
|
||||
score += 0.06;
|
||||
}
|
||||
|
||||
return Math.max(0.0, Math.min(1.0, score));
|
||||
}
|
||||
|
||||
function computeTextMatchScore(s1, s2) {
|
||||
if (s1 === s2) return 1.0;
|
||||
|
||||
let dist = levenshteinDistance(s1, s2);
|
||||
let maxLen = max2(s1.length, s2.length);
|
||||
if (maxLen === 0) return 1.0;
|
||||
|
||||
let full = 1.0 - (dist / maxLen);
|
||||
let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1);
|
||||
|
||||
let score = 0.4 * full + 0.6 * part;
|
||||
|
||||
let lenDiff = Math.abs(s1.length - s2.length);
|
||||
if (lenDiff >= 10) {
|
||||
score -= 0.02 * lenDiff / maxLen;
|
||||
}
|
||||
|
||||
let commonPrefixLen = 0;
|
||||
let minLen = min2(s1.length, s2.length);
|
||||
for (let i = 0; i < minLen; i++) {
|
||||
if (s1[i] === s2[i]) {
|
||||
commonPrefixLen++;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
score += 0.01 * commonPrefixLen;
|
||||
|
||||
if (s1.includes(s2) || s2.includes(s1)) {
|
||||
score += 0.2;
|
||||
}
|
||||
|
||||
return Math.max(0.0, Math.min(1.0, score));
|
||||
}
|
||||
200
surfaces/quickshell/ii-base/modules/common/functions/myers.js
Normal file
200
surfaces/quickshell/ii-base/modules/common/functions/myers.js
Normal file
|
|
@ -0,0 +1,200 @@
|
|||
// Bit-parallel approximate string matching — Myers (1999).
|
||||
// Time: O(n · ⌈m/WORD_SIZE⌉) after prepare(); O(n) for typical short queries.
|
||||
|
||||
// 30, not 32: JS bitwise ops work on signed 32-bit integers, so
|
||||
// (1 << 31) = -2147483648 and (1 << 32) wraps to 1. 30 keeps all
|
||||
// mask values positive and avoids sign-bit surprises throughout.
|
||||
const WORD_SIZE = 30;
|
||||
|
||||
// ─── Normalization ────────────────────────────────────────────────────────────
|
||||
|
||||
// Single normalization function used by every public API.
|
||||
// NFC composes canonically equivalent sequences so "é" and "é"
|
||||
// use the same representation and hash to the same character in Peq.
|
||||
// Guard against older Qt/QML runtimes where String.normalize may be absent.
|
||||
function normalize(value) {
|
||||
const str = String(value == null ? "" : value).toLowerCase();
|
||||
return typeof str.normalize === "function" ? str.normalize("NFC") : str;
|
||||
}
|
||||
|
||||
// ─── Public API ───────────────────────────────────────────────────────────────
|
||||
|
||||
// Precompute equality bitmasks for a pattern.
|
||||
// Call once per query; reuse the result across all candidate comparisons.
|
||||
function prepare(pattern) {
|
||||
const norm = normalize(pattern);
|
||||
const m = norm.length;
|
||||
if (m === 0) return { Peq: [], m: 0, words: 0 };
|
||||
|
||||
const words = Math.ceil(m / WORD_SIZE);
|
||||
const Peq = new Array(words);
|
||||
for (let w = 0; w < words; w++) Peq[w] = Object.create(null);
|
||||
|
||||
for (let i = 0; i < m; i++) {
|
||||
const c = norm[i];
|
||||
const w = (i / WORD_SIZE) | 0;
|
||||
Peq[w][c] = (Peq[w][c] | 0) | (1 << (i % WORD_SIZE));
|
||||
}
|
||||
|
||||
return { Peq, m, words };
|
||||
}
|
||||
|
||||
// Raw edit distance. text must already be normalized.
|
||||
// Keeping normalization out of the hot path lets callers normalize once per batch.
|
||||
// Use distance() or score() if you want automatic normalization.
|
||||
function distanceNormalized(prepared, text) {
|
||||
const { Peq, m, words } = prepared;
|
||||
const n = text.length;
|
||||
if (m === 0) return n;
|
||||
if (n === 0) return m;
|
||||
return words === 1
|
||||
? _distSingle(Peq[0], m, text, n)
|
||||
: _distMulti(Peq, m, words, text, n);
|
||||
}
|
||||
|
||||
// Edit distance with automatic text normalization. Safe to call with any string.
|
||||
function distance(prepared, text) {
|
||||
return distanceNormalized(prepared, normalize(text));
|
||||
}
|
||||
|
||||
// Normalized similarity in [0, 1]. 1 = identical, 0 = completely different.
|
||||
function score(prepared, text) {
|
||||
const normText = normalize(text);
|
||||
const maxLen = Math.max(prepared.m, normText.length);
|
||||
if (maxLen === 0) return 1.0;
|
||||
return 1.0 - distanceNormalized(prepared, normText) / maxLen;
|
||||
}
|
||||
|
||||
// One-off score without a separate prepare() call.
|
||||
function computeScore(pattern, text) {
|
||||
return score(prepare(pattern), text);
|
||||
}
|
||||
|
||||
// Drop-in replacement for levendist.js computeTextMatchScore.
|
||||
function computeTextMatchScore(s1, s2) {
|
||||
return computeScore(s1, s2);
|
||||
}
|
||||
|
||||
// Rank candidates by descending similarity.
|
||||
// opts.key — property name to extract text from each candidate (null = use candidate directly)
|
||||
// opts.threshold — minimum score to include; results with score <= threshold are dropped.
|
||||
// Uses > not >= so threshold=0 means "include anything with a positive score."
|
||||
function search(prepared, candidates, opts) {
|
||||
const key = opts != null ? opts.key : null;
|
||||
const threshold = opts != null && opts.threshold != null ? opts.threshold : 0;
|
||||
const len = candidates.length;
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < len; i++) {
|
||||
const c = candidates[i];
|
||||
const raw = key != null ? c[key] : c;
|
||||
const s = score(prepared, raw);
|
||||
if (s > threshold) results.push({ item: c, s: s, i: i });
|
||||
}
|
||||
|
||||
// Secondary sort by original index keeps equal-score results stable
|
||||
// so the list doesn't visually shuffle between keystrokes.
|
||||
results.sort((a, b) => (b.s - a.s) || (a.i - b.i));
|
||||
|
||||
const out = new Array(results.length);
|
||||
for (let i = 0; i < results.length; i++) out[i] = results[i].item;
|
||||
return out;
|
||||
}
|
||||
|
||||
// ─── Single-word Myers (m ≤ WORD_SIZE = 30) ─────────────────────────────────
|
||||
|
||||
function _distSingle(peq, m, text, n) {
|
||||
const FULL = (1 << m) - 1;
|
||||
const TOP = 1 << (m - 1);
|
||||
let Pv = FULL, Mv = 0, score = m;
|
||||
|
||||
for (let j = 0; j < n; j++) {
|
||||
const Eq = peq[text[j]] | 0;
|
||||
const Xv = Eq | Mv;
|
||||
const Xh = (((Eq & Pv) + Pv) ^ Pv) | Eq;
|
||||
|
||||
let Ph = (Mv | ~(Xh | Pv)) & FULL;
|
||||
let Mh = Pv & Xh & FULL;
|
||||
|
||||
if (Ph & TOP) score++;
|
||||
if (Mh & TOP) score--;
|
||||
|
||||
Ph = ((Ph << 1) | 1) & FULL;
|
||||
Mh = (Mh << 1) & FULL;
|
||||
|
||||
Pv = (Mh | ~(Xv | Ph)) & FULL;
|
||||
Mv = Ph & Xv & FULL;
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
// ─── Multi-word Myers (m > WORD_SIZE) ───────────────────────────────────────
|
||||
//
|
||||
// Shared scratch buffers to avoid per-call allocation.
|
||||
// Safe for synchronous JS; do not call re-entrantly (e.g. from a callback inside distance()).
|
||||
|
||||
let _sharedPv = new Int32Array(16);
|
||||
let _sharedMv = new Int32Array(16);
|
||||
let _sharedFull = new Int32Array(16);
|
||||
|
||||
function _distMulti(Peq, m, words, text, n) {
|
||||
const lastBits = m % WORD_SIZE || WORD_SIZE;
|
||||
|
||||
if (words > _sharedPv.length) {
|
||||
const newSize = Math.max(words, _sharedPv.length * 2);
|
||||
_sharedPv = new Int32Array(newSize);
|
||||
_sharedMv = new Int32Array(newSize);
|
||||
_sharedFull = new Int32Array(newSize);
|
||||
}
|
||||
|
||||
for (let w = 0; w < words - 1; w++) _sharedFull[w] = (1 << WORD_SIZE) - 1;
|
||||
_sharedFull[words - 1] = (1 << lastBits) - 1;
|
||||
|
||||
const LAST_TOP = 1 << (lastBits - 1);
|
||||
|
||||
for (let w = 0; w < words; w++) {
|
||||
_sharedPv[w] = _sharedFull[w];
|
||||
_sharedMv[w] = 0;
|
||||
}
|
||||
|
||||
let score = m;
|
||||
|
||||
for (let j = 0; j < n; j++) {
|
||||
const c = text[j];
|
||||
let addCarry = 0, phCarry = 1, mhCarry = 0;
|
||||
|
||||
for (let w = 0; w < words; w++) {
|
||||
const fw = _sharedFull[w];
|
||||
const Eq = Peq[w][c] | 0;
|
||||
const Xv = (Eq | _sharedMv[w]) & fw;
|
||||
|
||||
const sumBig = (Eq & _sharedPv[w]) + _sharedPv[w] + addCarry;
|
||||
addCarry = sumBig > fw ? 1 : 0;
|
||||
|
||||
const Xh = (((sumBig & fw) ^ _sharedPv[w]) | Eq) & fw;
|
||||
|
||||
let Ph = (_sharedMv[w] | ~(Xh | _sharedPv[w])) & fw;
|
||||
let Mh = _sharedPv[w] & Xh & fw;
|
||||
|
||||
if (w === words - 1) {
|
||||
if (Ph & LAST_TOP) score++;
|
||||
if (Mh & LAST_TOP) score--;
|
||||
}
|
||||
|
||||
const phTop = (Ph >>> (WORD_SIZE - 1)) & 1;
|
||||
const mhTop = (Mh >>> (WORD_SIZE - 1)) & 1;
|
||||
|
||||
Ph = ((Ph << 1) | phCarry) & fw;
|
||||
Mh = ((Mh << 1) | mhCarry) & fw;
|
||||
|
||||
phCarry = phTop;
|
||||
mhCarry = mhTop;
|
||||
|
||||
_sharedPv[w] = (Mh | ~(Xv | Ph)) & fw;
|
||||
_sharedMv[w] = Ph & Xv & fw;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
/**
|
||||
* Material color scheme adapted to a given color. It's incomplete but enough for what we need...
|
||||
*/
|
||||
QtObject {
|
||||
id: root
|
||||
required property color color
|
||||
readonly property bool colorIsDark: color.hslLightness < 0.5
|
||||
|
||||
property color colLayer0: ColorUtils.mix(Appearance.colors.colLayer0, root.color, (colorIsDark && Appearance.m3colors.darkmode) ? 0.6 : 0.5)
|
||||
property color colLayer1: ColorUtils.mix(Appearance.colors.colLayer1, root.color, 0.5)
|
||||
property color colOnLayer0: ColorUtils.mix(Appearance.colors.colOnLayer0, root.color, 0.5)
|
||||
property color colOnLayer1: ColorUtils.mix(Appearance.colors.colOnLayer1, root.color, 0.5)
|
||||
property color colSubtext: ColorUtils.mix(Appearance.colors.colOnLayer1, root.color, 0.5)
|
||||
property color colPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimary, root.color), root.color, 0.5)
|
||||
property color colPrimaryHover: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryHover, root.color), root.color, 0.3)
|
||||
property color colPrimaryActive: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colPrimaryActive, root.color), root.color, 0.3)
|
||||
property color colSecondary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.colors.colSecondary, root.color), root.color, 0.5)
|
||||
property color colSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3secondaryContainer, root.color, 0.15)
|
||||
property color colSecondaryContainerHover: ColorUtils.mix(Appearance.colors.colSecondaryContainerHover, root.color, 0.3)
|
||||
property color colSecondaryContainerActive: ColorUtils.mix(Appearance.colors.colSecondaryContainerActive, root.color, 0.5)
|
||||
property color colOnPrimary: ColorUtils.mix(ColorUtils.adaptToAccent(Appearance.m3colors.m3onPrimary, root.color), root.color, 0.5)
|
||||
property color colOnSecondaryContainer: ColorUtils.mix(Appearance.m3colors.m3onSecondaryContainer, root.color, 0.5)
|
||||
}
|
||||
|
|
@ -0,0 +1,26 @@
|
|||
import QtQuick
|
||||
|
||||
// idx1 is the "leading" indicator position, idx2 is the "following" one
|
||||
// The former animates faster than the latter, see the NumberAnimations below
|
||||
QtObject {
|
||||
id: root
|
||||
required property int index
|
||||
|
||||
property real idx1: index
|
||||
property real idx2: index
|
||||
property int idx1Duration: 100
|
||||
property int idx2Duration: 300
|
||||
|
||||
Behavior on idx1 {
|
||||
NumberAnimation {
|
||||
duration: root.idx1Duration
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
}
|
||||
Behavior on idx2 {
|
||||
NumberAnimation {
|
||||
duration: root.idx2Duration
|
||||
easing.type: Easing.OutSine
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,53 @@
|
|||
import QtQuick
|
||||
import Qt.labs.folderlistmodel
|
||||
|
||||
FolderListModel {
|
||||
id: root
|
||||
property list<url> folderHistory: []
|
||||
property int currentFolderHistoryIndex: -1
|
||||
property bool historyNavigationLock: false
|
||||
|
||||
function lockNextNavigation() {
|
||||
historyNavigationLock = true;
|
||||
}
|
||||
|
||||
function pushToHistory(path) {
|
||||
if (folderHistory[currentFolderHistoryIndex] === path)
|
||||
return;
|
||||
folderHistory = folderHistory.slice(0, currentFolderHistoryIndex + 1);
|
||||
folderHistory.push(path);
|
||||
currentFolderHistoryIndex = folderHistory.length - 1;
|
||||
}
|
||||
|
||||
function navigateUp() {
|
||||
root.folder = root.parentFolder;
|
||||
}
|
||||
|
||||
function navigateBack() {
|
||||
if (currentFolderHistoryIndex === 0)
|
||||
return;
|
||||
currentFolderHistoryIndex--;
|
||||
lockNextNavigation();
|
||||
root.folder = folderHistory[currentFolderHistoryIndex];
|
||||
}
|
||||
|
||||
function navigateForward() {
|
||||
if (currentFolderHistoryIndex >= folderHistory.length - 1) return;
|
||||
currentFolderHistoryIndex++;
|
||||
lockNextNavigation();
|
||||
root.folder = folderHistory[currentFolderHistoryIndex];
|
||||
}
|
||||
|
||||
onFolderChanged: {
|
||||
if (historyNavigationLock) {
|
||||
historyNavigationLock = false;
|
||||
return;
|
||||
}
|
||||
pushToHistory(folder);
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
root.folderHistory = [root.folder]
|
||||
root.currentFolderHistoryIndex = 0
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import Quickshell
|
||||
|
||||
ScriptModel {
|
||||
required property int count
|
||||
values: Array(count).map((_, i) => i)
|
||||
}
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
QtObject {
|
||||
enum IconType { Material, Text, System, None }
|
||||
enum FontType { Normal, Monospace }
|
||||
|
||||
// General stuff
|
||||
property string type: ""
|
||||
property var fontType: LauncherSearchResult.FontType.Normal
|
||||
property string name: ""
|
||||
property string rawValue: ""
|
||||
property string iconName: ""
|
||||
property var iconType: LauncherSearchResult.IconType.None
|
||||
property string verb: ""
|
||||
property bool blurImage: false
|
||||
property var execute: () => {
|
||||
print("Not implemented");
|
||||
}
|
||||
property var actions: []
|
||||
|
||||
// Stuff needed for DesktopEntry
|
||||
property string id: ""
|
||||
property bool shown: true
|
||||
property string comment: ""
|
||||
property bool runInTerminal: false
|
||||
property string genericName: ""
|
||||
property list<string> keywords: []
|
||||
|
||||
// Extra stuff to allow for more flexibility
|
||||
property string category: type
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
import QtQuick
|
||||
|
||||
// QtObject that allows stuff to be freely declared inside
|
||||
QtObject {
|
||||
default property list<QtObject> data
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.utils
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import ".."
|
||||
|
||||
NestableObject {
|
||||
id: root
|
||||
|
||||
enum State {
|
||||
Done, Preparing, Processing, Error
|
||||
}
|
||||
|
||||
signal finished()
|
||||
signal error(message: string)
|
||||
property int errorCode
|
||||
property string errorMessage: ""
|
||||
property var outputData
|
||||
property var state: GCloudApi.State.Done
|
||||
|
||||
function resetState() {
|
||||
root.state = GCloudApi.State.Done;
|
||||
root.errorMessage = "";
|
||||
root.outputData = undefined;
|
||||
}
|
||||
|
||||
function handleApiOutput(out: string): bool {
|
||||
try {
|
||||
root.outputData = JSON.parse(out);
|
||||
if (outputData.error) {
|
||||
print("API error: " + JSON.stringify(outputData.error, null, 2))
|
||||
root.state = GCloudApi.State.Error;
|
||||
root.errorCode = outputData.error.code;
|
||||
root.errorMessage = outputData.error.message;
|
||||
root.error(outputData.error.message);
|
||||
return false;
|
||||
}
|
||||
root.finished();
|
||||
root.state = GCloudApi.State.Done;
|
||||
return true
|
||||
} catch (e) {
|
||||
print("Failed to parse API response: " + e + "\n" + out)
|
||||
root.state = GCloudApi.State.Error;
|
||||
root.errorMessage = "Failed to parse API response";
|
||||
root.error(root.errorMessage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,57 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.utils
|
||||
import qs.services
|
||||
import ".."
|
||||
|
||||
GCloudApi {
|
||||
id: root
|
||||
|
||||
property list<string> pendingStrings
|
||||
property bool setupReady: false
|
||||
readonly property bool preparationReady: GoogleCloud.tokenReady && setupReady
|
||||
|
||||
function translateStrings(strings: list<string>) {
|
||||
GoogleCloud.load();
|
||||
root.setupReady = false;
|
||||
root.pendingStrings = strings;
|
||||
root.state = GCloudApi.State.Preparing;
|
||||
root.setupReady = true;
|
||||
}
|
||||
|
||||
onPreparationReadyChanged: {
|
||||
if (!preparationReady) return;
|
||||
root.state = GCloudApi.State.Processing;
|
||||
|
||||
const targetLang = Translation.languageCode;
|
||||
const payload = {
|
||||
"targetLanguageCode": targetLang,
|
||||
"contents": root.pendingStrings,
|
||||
"mimeType": "text/plain"
|
||||
};
|
||||
|
||||
// print("PENDING STRINGS:", root.pendingStrings)
|
||||
|
||||
var seq = [];
|
||||
seq.push([ //
|
||||
"bash", "-c", //
|
||||
`curl -sL -X POST \
|
||||
-H "Authorization: Bearer ${GoogleCloud.token}" \
|
||||
-H "x-goog-user-project: ${GoogleCloud.projectId}" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '${StringUtils.shellSingleQuoteEscape(JSON.stringify(payload))}' \
|
||||
"https://translation.googleapis.com/v3/projects/${GoogleCloud.projectId}:translateText"`
|
||||
]);
|
||||
|
||||
seq.push(((out) => {
|
||||
root.handleApiOutput(out);
|
||||
}));
|
||||
|
||||
multiproc.runSequence(seq);
|
||||
}
|
||||
|
||||
MultiTurnProcess {
|
||||
id: multiproc
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.utils
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import ".."
|
||||
|
||||
GCloudApi {
|
||||
id: root
|
||||
|
||||
readonly property string imageBase64FilePath: `${Directories.screenshotTemp}/vision_base64.txt`
|
||||
readonly property string payloadFilePath: `${Directories.screenshotTemp}/vision_payload.json`
|
||||
property string uploadEndpoint: "https://uguu.se/upload"
|
||||
|
||||
property bool tokenReady: GoogleCloud.tokenReady
|
||||
property bool onlineImageReady: false
|
||||
readonly property bool preparationReady: tokenReady && onlineImageReady
|
||||
|
||||
function annotateImage(imageUri: string) {
|
||||
resetState();
|
||||
root.state = GCloudApi.State.Preparing;
|
||||
root.onlineImageReady = false
|
||||
GoogleCloud.load();
|
||||
|
||||
var seq = []; // command sequence
|
||||
|
||||
const niceFilePath = StringUtils.shellSingleQuoteEscape(FileUtils.trimFileProtocol(imageUri))
|
||||
seq = [ //
|
||||
["bash", "-c", `mkdir -p '${Directories.screenshotTemp}'; base64 '${niceFilePath}' -w 0 > '${imageBase64FilePath}'`], //
|
||||
(out) => { //
|
||||
root.onlineImageReady = true; //
|
||||
}
|
||||
]
|
||||
|
||||
// Execute the base64 conversion & load the token
|
||||
prepMultiproc.runSequence(seq);
|
||||
}
|
||||
|
||||
onPreparationReadyChanged: {
|
||||
if (!preparationReady) return;
|
||||
if (GoogleCloud.tokenError || GoogleCloud.keyError) {
|
||||
root.state = GCloudApi.State.Error;
|
||||
root.error(Translation.tr("Set your Google Cloud service account key"));
|
||||
return;
|
||||
}
|
||||
root.state = GCloudApi.State.Processing;
|
||||
var seq = []; // command sequence
|
||||
|
||||
// Construct the JSON payload using jq to read from the base64 file
|
||||
seq.push([
|
||||
"bash", "-c",
|
||||
`jq -n --rawfile content '${imageBase64FilePath}' \
|
||||
'{"requests": [{"image": {"content": $content}, "features": [{"type": "DOCUMENT_TEXT_DETECTION"}]}]}' \
|
||||
> '${payloadFilePath}'`
|
||||
]);
|
||||
|
||||
seq.push([
|
||||
"bash", "-c",
|
||||
`curl -s -X POST \
|
||||
-H "Authorization: Bearer ${GoogleCloud.token}" \
|
||||
-H "x-goog-user-project: ${GoogleCloud.projectId}" \
|
||||
-H "Content-Type: application/json" \
|
||||
https://vision.googleapis.com/v1/images:annotate \
|
||||
-d @'${payloadFilePath}'`
|
||||
]);
|
||||
|
||||
seq.push((out) => {
|
||||
root.handleApiOutput(out);
|
||||
});
|
||||
|
||||
lookMultiproc.runSequence(seq);
|
||||
}
|
||||
|
||||
MultiTurnProcess {
|
||||
id: prepMultiproc
|
||||
}
|
||||
|
||||
MultiTurnProcess {
|
||||
id: lookMultiproc
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import ".."
|
||||
|
||||
NestableObject {
|
||||
id: root
|
||||
|
||||
property real confidenceThreshold: 0.5 // TODO tune this
|
||||
|
||||
property var rawData
|
||||
property var rawBlocks
|
||||
property var rawParagraphs
|
||||
property var coherentParagraphs
|
||||
|
||||
function initializeWithData(apiOutputData: var): void {
|
||||
// Null check
|
||||
if (!apiOutputData) {
|
||||
print("[GCloudVisionResult] Data is null/undefined")
|
||||
return;
|
||||
}
|
||||
|
||||
// Raw data
|
||||
root.rawData = apiOutputData
|
||||
|
||||
// Raw blocks
|
||||
var pages = apiOutputData.responses[0].fullTextAnnotation.pages
|
||||
var blocks = [];
|
||||
for (var i = 0; i < pages.length; i++) {
|
||||
// print("this page", JSON.stringify(pages[i]))
|
||||
var blocksThisPage = pages[i].blocks;
|
||||
for (var j = 0; j < blocksThisPage.length; j++) {
|
||||
const block = blocksThisPage[j];
|
||||
// print("new block with confidence", block.confidence, ":", JSON.stringify(block, null, 2))
|
||||
if (block.confidence > root.confidenceThreshold) {
|
||||
blocks.push(block);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
root.rawBlocks = blocks
|
||||
// print("RAW BLOCKS:", blocks)
|
||||
|
||||
// Raw paragraphs
|
||||
var paragraphs = []
|
||||
for (var i = 0; i < blocks.length; i++) {
|
||||
var blockParagraphs = blocks[i].paragraphs;
|
||||
for (var j = 0; j < blockParagraphs.length; j++) {
|
||||
const para = blockParagraphs[j];
|
||||
// print("new paragraph", JSON.stringify(para))
|
||||
paragraphs.push(para);
|
||||
}
|
||||
}
|
||||
root.rawParagraphs = [...paragraphs];
|
||||
|
||||
// print("RAW PARAGRAPHS", paragraphs)
|
||||
|
||||
// Coherent paragraphs
|
||||
// (raw data can be as granular as symbols)
|
||||
// We're interested in paragraph level of granularity as it's good for translations
|
||||
for (var i = 0; i < paragraphs.length; i++) {
|
||||
const paragraph = paragraphs[i];
|
||||
const words = paragraph.words;
|
||||
var strList = []
|
||||
for (var j = 0; j < words.length; j++) {
|
||||
const symbols = words[j].symbols;
|
||||
for (var k = 0; k < symbols.length; k++) {
|
||||
const sym = symbols[k];
|
||||
strList.push(sym.text);
|
||||
// print("CHAR:", JSON.stringify(sym, null, 2));
|
||||
// Breaks
|
||||
// Reference: https://docs.cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#breaktype
|
||||
if (sym.property?.detectedBreak.type == "SPACE" || sym.property?.detectedBreak.type == "UNKNOWN") {
|
||||
strList.push(" ");
|
||||
} else if (sym.property?.detectedBreak.type == "SURE_SPACE") {
|
||||
strList.push(" ");
|
||||
} else if (sym.property?.detectedBreak.type == "EOL_SURE_SPACE" || sym.property?.detectedBreak.type == "LINE_BREAK") {
|
||||
strList.push("\n");
|
||||
} else if (sym.property?.detectedBreak.type == "HYPHEN") {
|
||||
strList.push("-\n");
|
||||
}
|
||||
}
|
||||
}
|
||||
// print("STR LIST:", strList)
|
||||
paragraphs[i].text = strList.join("").trim();
|
||||
// print("PARA TEXT:", paragraphs[i].text)
|
||||
}
|
||||
root.coherentParagraphs = paragraphs
|
||||
// print("COHERENT PARAGRAPHS", JSON.stringify(paragraphs))
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,63 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.services
|
||||
import "../"
|
||||
|
||||
NestableObject {
|
||||
id: root
|
||||
|
||||
required property string key
|
||||
property alias fetching: fetchProc.running
|
||||
property bool set
|
||||
property var value
|
||||
|
||||
Component.onCompleted: fetch()
|
||||
|
||||
Connections {
|
||||
target: HyprlandConfig
|
||||
function onReloaded() {
|
||||
root.fetch();
|
||||
}
|
||||
}
|
||||
|
||||
function fetch() {
|
||||
fetchProc.command = fetchProc.baseCommand.concat([root.key]);
|
||||
fetchProc.running = true;
|
||||
}
|
||||
|
||||
function setValue(newValue) {
|
||||
HyprlandConfig.set(root.key, newValue)
|
||||
}
|
||||
|
||||
function reset() {
|
||||
HyprlandConfig.reset(root.key)
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchProc
|
||||
property list<string> baseCommand: ["hyprctl", "getoption", "-j"]
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
if (text == "no such option")
|
||||
return;
|
||||
try {
|
||||
const obj = JSON.parse(text);
|
||||
// Note that the value is returned as "<data type>": <value>
|
||||
// It's the only field that isn't always in the same key so we put it in an else
|
||||
for (const key in obj) {
|
||||
if (key == "option")
|
||||
continue;
|
||||
else if (key == "set")
|
||||
root.set = obj[key];
|
||||
else
|
||||
root.value = obj[key];
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(`[HyprlandConfigOption] Failed to fetch option "${root.key}":\n - Output: ${text.trim()}\n - Error: ${e}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: HyprlandAntiFlashbangShader.enabled ? (HyprlandAntiFlashbangShader.weak ? Translation.tr("Anti-flash: Weak") : Translation.tr("Anti-flash: Strong")) : Translation.tr("Anti-flashbang")
|
||||
tooltipText: `${Translation.tr("Anti-flashbang")}: ${HyprlandAntiFlashbangShader.enabled ? (HyprlandAntiFlashbangShader.weak ? Translation.tr("Weak") : Translation.tr("Strong")) : Translation.tr("Off")}`
|
||||
icon: HyprlandAntiFlashbangShader.enabled ? (!HyprlandAntiFlashbangShader.weak ? "flash_off" : "sunny_snowing") : "flash_on"
|
||||
toggled: HyprlandAntiFlashbangShader.enabled
|
||||
|
||||
mainAction: () => {
|
||||
HyprlandAntiFlashbangShader.cycle()
|
||||
}
|
||||
hasMenu: true
|
||||
}
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Audio output")
|
||||
statusText: toggled ? Translation.tr("Unmuted") : Translation.tr("Muted")
|
||||
tooltipText: Translation.tr("Audio output | Right-click for volume mixer & device selector")
|
||||
toggled: !Audio.sink?.audio?.muted
|
||||
icon: Audio.sink?.audio?.muted ? "volume_off" : "volume_up"
|
||||
mainAction: () => {
|
||||
Audio.toggleMute()
|
||||
}
|
||||
hasMenu: true
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import QtQuick
|
||||
import Quickshell.Bluetooth
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Bluetooth")
|
||||
statusText: BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Not connected")
|
||||
tooltipText: Translation.tr("%1 | Right-click to configure").arg(
|
||||
(BluetoothStatus.firstActiveDevice?.name ?? Translation.tr("Bluetooth"))
|
||||
+ (BluetoothStatus.activeDeviceCount > 1 ? ` +${BluetoothStatus.activeDeviceCount - 1}` : "")
|
||||
)
|
||||
icon: BluetoothStatus.connected ? "bluetooth_connected" : BluetoothStatus.enabled ? "bluetooth" : "bluetooth_disabled"
|
||||
|
||||
available: BluetoothStatus.available
|
||||
toggled: BluetoothStatus.enabled
|
||||
mainAction: () => {
|
||||
Bluetooth.defaultAdapter.enabled = !Bluetooth.defaultAdapter?.enabled
|
||||
}
|
||||
hasMenu: true
|
||||
}
|
||||
|
|
@ -0,0 +1,78 @@
|
|||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
QuickToggleModel {
|
||||
id: root
|
||||
name: Translation.tr("Cloudflare WARP")
|
||||
|
||||
toggled: false
|
||||
icon: "cloud_lock"
|
||||
|
||||
mainAction: () => {
|
||||
if (toggled) {
|
||||
root.toggled = false
|
||||
Quickshell.execDetached(["warp-cli", "disconnect"])
|
||||
} else {
|
||||
root.toggled = true
|
||||
Quickshell.execDetached(["warp-cli", "connect"])
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: connectProc
|
||||
command: ["warp-cli", "connect"]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0) {
|
||||
Quickshell.execDetached(["notify-send",
|
||||
Translation.tr("Cloudflare WARP"),
|
||||
Translation.tr("Connection failed. Please inspect manually with the <tt>warp-cli</tt> command")
|
||||
, "-a", "Shell"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: registrationProc
|
||||
command: ["warp-cli", "registration", "new"]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
console.log("Warp registration exited with code and status:", exitCode, exitStatus)
|
||||
if (exitCode === 0) {
|
||||
connectProc.running = true
|
||||
} else {
|
||||
Quickshell.execDetached(["notify-send",
|
||||
Translation.tr("Cloudflare WARP"),
|
||||
Translation.tr("Registration failed. Please inspect manually with the <tt>warp-cli</tt> command"),
|
||||
"-a", "Shell"
|
||||
])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fetchActiveState
|
||||
running: true
|
||||
command: ["bash", "-c", "warp-cli status"]
|
||||
stdout: StdioCollector {
|
||||
id: warpStatusCollector
|
||||
onStreamFinished: {
|
||||
if (warpStatusCollector.text.length > 0) {
|
||||
root.available = true
|
||||
}
|
||||
if (warpStatusCollector.text.includes("Unable")) {
|
||||
registrationProc.running = true
|
||||
} else if (warpStatusCollector.text.includes("Connected")) {
|
||||
root.toggled = true
|
||||
} else if (warpStatusCollector.text.includes("Disconnected")) {
|
||||
root.toggled = false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
tooltipText: Translation.tr("Cloudflare WARP (1.1.1.1)")
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Color picker")
|
||||
hasStatusText: false
|
||||
toggled: false
|
||||
icon: "colorize"
|
||||
|
||||
mainAction: () => {
|
||||
GlobalStates.sidebarRightOpen = false;
|
||||
delayedActionTimer.start();
|
||||
}
|
||||
Timer {
|
||||
id: delayedActionTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Quickshell.execDetached(["hyprpicker", "-a"]);
|
||||
}
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Color picker")
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Dark Mode")
|
||||
statusText: Appearance.m3colors.darkmode ? Translation.tr("Dark") : Translation.tr("Light")
|
||||
|
||||
toggled: Appearance.m3colors.darkmode
|
||||
icon: "contrast"
|
||||
|
||||
mainAction: () => {
|
||||
if (Appearance.m3colors.darkmode) {
|
||||
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "light", "--noswitch"]);
|
||||
} else {
|
||||
Quickshell.execDetached([Directories.wallpaperSwitchScriptPath, "--mode", "dark", "--noswitch"]);
|
||||
}
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Dark Mode")
|
||||
}
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("EasyEffects")
|
||||
|
||||
available: EasyEffects.available
|
||||
toggled: EasyEffects.active
|
||||
icon: "graphic_eq"
|
||||
|
||||
Component.onCompleted: {
|
||||
EasyEffects.fetchActiveState()
|
||||
}
|
||||
|
||||
mainAction: () => {
|
||||
EasyEffects.toggle()
|
||||
}
|
||||
|
||||
altAction: () => {
|
||||
Quickshell.execDetached(["bash", "-c", "flatpak run com.github.wwmm.easyeffects || easyeffects"])
|
||||
GlobalStates.sidebarRightOpen = false
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("EasyEffects | Right-click to configure")
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
|
||||
/**
|
||||
* Whole-screen capture, as opposed to ScreenSnipToggle's region select.
|
||||
*
|
||||
* `fullScreenshot` had been listed in the phone's config for long enough that
|
||||
* everyone assumed a component existed; none ever did, in any tree or any
|
||||
* commit. It rendered nothing because there was nothing to render.
|
||||
*/
|
||||
QuickToggleModel {
|
||||
id: root
|
||||
name: Translation.tr("Screenshot")
|
||||
hasStatusText: false
|
||||
toggled: false
|
||||
icon: "screenshot_monitor"
|
||||
|
||||
// Close the panel first, or the screenshot is a picture of the panel.
|
||||
// Same 300 ms the region snip uses — the close animation has to finish.
|
||||
mainAction: () => {
|
||||
GlobalStates.sidebarRightOpen = false;
|
||||
captureDelay.start();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: captureDelay
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: captureProc.running = true
|
||||
}
|
||||
|
||||
// To a file AND the clipboard: on the phone there is rarely anywhere to
|
||||
// paste it right away, and on the laptop the file is the annoying half.
|
||||
Process {
|
||||
id: captureProc
|
||||
command: ["bash", "-c",
|
||||
"mkdir -p \"$HOME/Pictures\"; "
|
||||
+ "f=\"$HOME/Pictures/screenshot-$(date +%Y%m%d-%H%M%S).png\"; "
|
||||
+ "grim \"$f\" || { notify-send Screenshot 'Capture failed.' -a Shell; exit 1; }; "
|
||||
+ "wl-copy -t image/png < \"$f\"; "
|
||||
+ "notify-send Screenshot \"${f##*/} — saved and copied\" -a Shell"]
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Whole screen to ~/Pictures and the clipboard")
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import qs.modules.common.models.hyprland
|
||||
import qs.services
|
||||
|
||||
QuickToggleModel {
|
||||
id: root
|
||||
name: Translation.tr("Game mode")
|
||||
toggled: !confOpt.value
|
||||
icon: "gamepad"
|
||||
|
||||
mainAction: () => {
|
||||
root.toggled = !root.toggled;
|
||||
if (root.toggled) {
|
||||
HyprlandConfig.setMany({
|
||||
"animations:enabled": 0,
|
||||
"decoration:shadow:enabled": 0,
|
||||
"decoration:blur:enabled": 0,
|
||||
"general:gaps_in": 0,
|
||||
"general:gaps_out": 0,
|
||||
"general:border_size": 1,
|
||||
"decoration:rounding": 0,
|
||||
"general:allow_tearing": 1
|
||||
});
|
||||
} else {
|
||||
HyprlandConfig.resetMany([ //
|
||||
"animations:enabled", //
|
||||
"decoration:shadow:enabled", //
|
||||
"decoration:blur:enabled", //
|
||||
"general:gaps_in", //
|
||||
"general:gaps_out", //
|
||||
"general:border_size", //
|
||||
"decoration:rounding", //
|
||||
"general:allow_tearing", //
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
HyprlandConfigOption {
|
||||
id: confOpt
|
||||
key: "animations:enabled"
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Game mode")
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
toggled: Idle.inhibit
|
||||
property bool autoIdleInhibit: Idle.autoIdleInhibit
|
||||
|
||||
name: Translation.tr("Keep awake")
|
||||
statusText: autoIdleInhibit ? Translation.tr("auto Idle: enabled") : Translation.tr("auto Idle: disabled")
|
||||
icon: autoIdleInhibit ? "emoji_food_beverage" : "coffee"
|
||||
|
||||
tooltipText: Translation.tr("Keep system awake | Right click to toggle auto mode")
|
||||
|
||||
mainAction: () => {
|
||||
Idle.toggleInhibit()
|
||||
}
|
||||
altAction: () => {
|
||||
Idle.toggleAutoInhibit()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Audio input")
|
||||
statusText: toggled ? Translation.tr("Enabled") : Translation.tr("Muted")
|
||||
toggled: !Audio.source?.audio?.muted
|
||||
icon: Audio.source?.audio?.muted ? "mic_off" : "mic"
|
||||
mainAction: () => {
|
||||
Audio.toggleMicMute()
|
||||
}
|
||||
hasMenu: true
|
||||
|
||||
tooltipText: Translation.tr("Audio input | Right-click for volume mixer & device selector")
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
toggled: SongRec.running
|
||||
property bool sourceIsMonitor: SongRec.monitorSource === SongRec.MonitorSource.Monitor
|
||||
|
||||
name: Translation.tr("Identify Music")
|
||||
statusText: toggled ? Translation.tr("Listening...") : sourceIsMonitor ? Translation.tr("System sound") : Translation.tr("Microphone")
|
||||
icon: toggled ? "music_cast" : (sourceIsMonitor ? "music_note" : "frame_person_mic")
|
||||
|
||||
tooltipText: Translation.tr("Recognize music | Right-click to toggle source")
|
||||
|
||||
mainAction: () => {
|
||||
SongRec.toggleRunning()
|
||||
}
|
||||
altAction: () => {
|
||||
SongRec.toggleMonitorSource()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Internet")
|
||||
statusText: Network.networkName
|
||||
tooltipText: Translation.tr("%1 | Right-click to configure").arg(Network.networkName)
|
||||
icon: Network.materialSymbol
|
||||
|
||||
toggled: Network.wifiStatus !== "disabled"
|
||||
mainAction: () => Network.toggleWifi()
|
||||
hasMenu: true
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
property bool auto: Config.options.light.night.automatic
|
||||
|
||||
name: Translation.tr("Night Light")
|
||||
statusText: (auto ? Translation.tr("Auto, ") : "") + (toggled ? Translation.tr("Active") : Translation.tr("Inactive"))
|
||||
|
||||
toggled: Hyprsunset.temperatureActive
|
||||
icon: auto ? "night_sight_auto" : "bedtime"
|
||||
|
||||
mainAction: () => {
|
||||
Hyprsunset.toggleTemperature()
|
||||
}
|
||||
hasMenu: true
|
||||
|
||||
Component.onCompleted: {
|
||||
Hyprsunset.fetchState()
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Night Light | Right-click to configure")
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Notifications")
|
||||
statusText: toggled ? Translation.tr("Show") : Translation.tr("Silent")
|
||||
toggled: !Notifications.silent
|
||||
icon: toggled ? "notifications_active" : "notifications_paused"
|
||||
|
||||
mainAction: () => {
|
||||
Notifications.silent = !Notifications.silent;
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Show notifications")
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Virtual Keyboard")
|
||||
toggled: GlobalStates.oskOpen
|
||||
icon: toggled ? "keyboard_hide" : "keyboard"
|
||||
|
||||
mainAction: () => {
|
||||
GlobalStates.oskOpen = !GlobalStates.oskOpen
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("On-screen keyboard")
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Services.UPower
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Power Profile")
|
||||
toggled: PowerProfiles.profile !== PowerProfile.Balanced
|
||||
icon: switch(PowerProfiles.profile) {
|
||||
case PowerProfile.PowerSaver: return "energy_savings_leaf"
|
||||
case PowerProfile.Balanced: return "airwave"
|
||||
case PowerProfile.Performance: return "local_fire_department"
|
||||
}
|
||||
statusText: switch(PowerProfiles.profile) {
|
||||
case PowerProfile.PowerSaver: return "Power Saver"
|
||||
case PowerProfile.Balanced: return "Balanced"
|
||||
case PowerProfile.Performance: return "Performance"
|
||||
}
|
||||
|
||||
mainAction: () => {
|
||||
if (PowerProfiles.hasPerformanceProfile) {
|
||||
switch(PowerProfiles.profile) {
|
||||
case PowerProfile.PowerSaver: PowerProfiles.profile = PowerProfile.Balanced
|
||||
break;
|
||||
case PowerProfile.Balanced: PowerProfiles.profile = PowerProfile.Performance
|
||||
break;
|
||||
case PowerProfile.Performance: PowerProfiles.profile = PowerProfile.PowerSaver
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
PowerProfiles.profile = PowerProfiles.profile == PowerProfile.Balanced ? PowerProfile.PowerSaver : PowerProfile.Balanced
|
||||
}
|
||||
}
|
||||
tooltipText: Translation.tr("Click to cycle through power profiles")
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
import QtQuick
|
||||
|
||||
QtObject {
|
||||
// Textual info
|
||||
required property string name
|
||||
property string statusText
|
||||
property string tooltipText: ""
|
||||
property string icon: "close"
|
||||
|
||||
// State
|
||||
property bool hasStatusText: true
|
||||
property bool available: true
|
||||
property bool toggled: false
|
||||
|
||||
// Interactions
|
||||
required property var mainAction
|
||||
property bool hasMenu: false
|
||||
property var altAction: null
|
||||
|
||||
// Allow stuff like Processes to be declared freely
|
||||
default property list<QtObject> data
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
QuickToggleModel {
|
||||
name: Translation.tr("Screen snip")
|
||||
hasStatusText: false
|
||||
toggled: false
|
||||
icon: "screenshot_region"
|
||||
|
||||
mainAction: () => {
|
||||
GlobalStates.sidebarRightOpen = false;
|
||||
delayedActionTimer.start();
|
||||
}
|
||||
Timer {
|
||||
id: delayedActionTimer
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath(""), "ipc", "call", "region", "screenshot"]);
|
||||
}
|
||||
}
|
||||
|
||||
tooltipText: Translation.tr("Screen snip")
|
||||
}
|
||||
|
|
@ -0,0 +1,176 @@
|
|||
import QtQuick
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
/**
|
||||
* A WireGuard tunnel, toggled deliberately.
|
||||
*
|
||||
* DELIBERATE, NEVER AUTOMATIC — and that is the design, not a limitation.
|
||||
* An autonomous gate held this tunnel on 2026-07-30: it recycled 652 times in
|
||||
* 90 minutes, through doze tiers whose contract is "network fetchers stopped",
|
||||
* and the user could not switch it off because `nmcli connection down` fired
|
||||
* the NM dispatcher that immediately brought it back up. A control surface the
|
||||
* owner cannot overrule is not a control surface.
|
||||
*
|
||||
* So there is no daemon, no timer and no dispatcher hook deciding this. The
|
||||
* toggle is the whole of it. If tunnel posture later becomes something the
|
||||
* device decides for itself, it belongs in sessiond's state machine as an
|
||||
* Action (DEVICE-STATE-MACHINE §12) and reachable as a verb (doctrine §13) —
|
||||
* not as another actor off to the side.
|
||||
*
|
||||
* No privilege needed: polkit already lets the seat user activate a system
|
||||
* connection, verified on device.
|
||||
*/
|
||||
QuickToggleModel {
|
||||
id: root
|
||||
|
||||
/// The NetworkManager connection this drives. A property rather than a
|
||||
/// constant so a second profile does not need a second component.
|
||||
///
|
||||
/// Give each device its OWN peer. A profile that shares an address and a
|
||||
/// private key between two machines lets WireGuard connect only one of
|
||||
/// them at a time, and one dialling a port with a key the server no longer
|
||||
/// lists will send and never receive. Set this per deployment.
|
||||
property string connectionName: ""
|
||||
|
||||
/// True while an up/down is in flight, so a double tap cannot race itself.
|
||||
property bool busy: false
|
||||
|
||||
/// A host that only exists at the far end of the tunnel. Reaching it is the
|
||||
/// only honest proof the tunnel carries traffic.
|
||||
property string probeHost: ""
|
||||
|
||||
/// "off" | "connecting" | "limited" | "online".
|
||||
///
|
||||
/// NOT a bool, and that is the point. On 2026-07-31 this toggle read "On"
|
||||
/// for an hour while the tunnel was deaf: NetworkManager said `activated`,
|
||||
/// `wg` said 368 B received against 5.3 KiB sent, and every service behind
|
||||
/// it — STT, TTS, gitea — was unreachable. "The interface is up" and "the
|
||||
/// tunnel carries traffic" are different claims and only one of them is
|
||||
/// worth showing.
|
||||
///
|
||||
/// Sailfish models exactly this distinction (`MobileDataConnection.status`
|
||||
/// is Online / Limited / Connecting, never a bool) and Android refuses to
|
||||
/// let an unvalidated network win at all. `limited` is the state both have
|
||||
/// and we did not.
|
||||
///
|
||||
/// The probe is deliberately NOT an internet check. This is a split tunnel
|
||||
/// whose whole purpose is a private range that no public probe can
|
||||
/// see; a link that fails a general-internet probe may be exactly the one
|
||||
/// we want. The question is per-consumer — "can this carry *this*" — so we
|
||||
/// ask the far end directly.
|
||||
property string status: "off"
|
||||
|
||||
name: Translation.tr("VPN")
|
||||
icon: root.status === "online" ? "vpn_lock"
|
||||
: root.status === "limited" ? "vpn_lock_off"
|
||||
: "vpn_key"
|
||||
statusText: {
|
||||
if (root.busy)
|
||||
return Translation.tr("…");
|
||||
switch (root.status) {
|
||||
case "online": return Translation.tr("On");
|
||||
case "limited": return Translation.tr("No route");
|
||||
default: return Translation.tr("Off");
|
||||
}
|
||||
}
|
||||
tooltipText: {
|
||||
switch (root.status) {
|
||||
case "online":
|
||||
return Translation.tr("VPN — the private range routed over the tunnel");
|
||||
case "limited":
|
||||
return Translation.tr("VPN — connected but not carrying traffic. The tunnel is up and the far side is unreachable; check which link the handshake is leaving by.");
|
||||
default:
|
||||
return Translation.tr("VPN — off");
|
||||
}
|
||||
}
|
||||
|
||||
mainAction: () => {
|
||||
if (root.busy)
|
||||
return;
|
||||
root.busy = true;
|
||||
if (root.toggled)
|
||||
downProc.running = true;
|
||||
else
|
||||
upProc.running = true;
|
||||
}
|
||||
|
||||
function notify(body) {
|
||||
Quickshell.execDetached(["notify-send", Translation.tr("VPN"), body, "-a", "Shell"]);
|
||||
}
|
||||
|
||||
Process {
|
||||
id: upProc
|
||||
command: ["nmcli", "connection", "up", root.connectionName]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.busy = false;
|
||||
refreshProc.running = true;
|
||||
if (exitCode !== 0)
|
||||
root.notify(Translation.tr("Could not connect."));
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: downProc
|
||||
command: ["nmcli", "connection", "down", root.connectionName]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.busy = false;
|
||||
refreshProc.running = true;
|
||||
if (exitCode !== 0)
|
||||
root.notify(Translation.tr("Could not disconnect."));
|
||||
}
|
||||
}
|
||||
|
||||
// Read NM rather than tracking a local bool: the connection can also be
|
||||
// brought up or down from nmcli, from Settings, or by NM itself, and a
|
||||
// shadow copy that disagrees with the owning subsystem is the failure
|
||||
// doctrine §4 names ("never a hand-tracked bool").
|
||||
// Two questions, asked together because the answer to the first does not
|
||||
// imply the second: is the connection active, and does it carry traffic.
|
||||
// A single `nmcli` reply can only answer the first, which is how this
|
||||
// reported "On" through an hour of a dead tunnel.
|
||||
//
|
||||
// The probe is one UDP DNS round-trip to the far-side resolver with a 2 s
|
||||
// deadline — cheap enough for a 15 s poll, and it fails in exactly the case
|
||||
// that matters (interface up, nothing crossing it). `getent` is not used:
|
||||
// it would consult the whole resolver list and could be answered by a
|
||||
// nameserver on another link, which is the same conflation this is here to
|
||||
// end.
|
||||
Process {
|
||||
id: refreshProc
|
||||
running: true
|
||||
command: ["bash", "-c",
|
||||
`if ! nmcli -t -f NAME connection show --active | grep -qx '${root.connectionName}'; then echo off; exit 0; fi;`
|
||||
+ ` if timeout 2 bash -c 'echo > /dev/udp/${root.probeHost}/53' 2>/dev/null`
|
||||
+ ` && timeout 2 ping -c1 -W2 ${root.probeHost} >/dev/null 2>&1; then echo online; else echo limited; fi`]
|
||||
stdout: StdioCollector {
|
||||
id: stateCollector
|
||||
onStreamFinished: {
|
||||
const reply = stateCollector.text.trim();
|
||||
if (reply.length === 0)
|
||||
return;
|
||||
root.status = reply;
|
||||
// `toggled` stays the user-facing "is it switched on", so the
|
||||
// button still lights while the tunnel is limited — the state
|
||||
// is reported in statusText rather than by silently un-toggling
|
||||
// something the user turned on. Sailfish does the same: the
|
||||
// switch stays checked and the description says "Limited".
|
||||
root.toggled = (reply !== "off");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cheap resync for changes made outside the panel. One nmcli call every
|
||||
// 15 s while the shell runs; deliberately not a decision loop.
|
||||
Timer {
|
||||
interval: 15000
|
||||
running: true
|
||||
repeat: true
|
||||
onTriggered: {
|
||||
if (!root.busy)
|
||||
refreshProc.running = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,137 @@
|
|||
import qs
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Services.Pam
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
enum ActionEnum { Unlock, Poweroff, Reboot }
|
||||
|
||||
signal shouldReFocus()
|
||||
signal unlocked(targetAction: var)
|
||||
signal failed()
|
||||
|
||||
// These properties are in the context and not individual lock surfaces
|
||||
// so all surfaces can share the same state.
|
||||
property string currentText: ""
|
||||
property bool unlockInProgress: false
|
||||
property bool showFailure: false
|
||||
property bool fingerprintsConfigured: false
|
||||
property var targetAction: LockContext.ActionEnum.Unlock
|
||||
property bool alsoInhibitIdle: false
|
||||
|
||||
function resetTargetAction() {
|
||||
root.targetAction = LockContext.ActionEnum.Unlock;
|
||||
}
|
||||
|
||||
function clearText() {
|
||||
root.currentText = "";
|
||||
}
|
||||
|
||||
function resetClearTimer() {
|
||||
passwordClearTimer.restart();
|
||||
}
|
||||
|
||||
function reset() {
|
||||
root.resetTargetAction();
|
||||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
stopFingerPam();
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: passwordClearTimer
|
||||
interval: 10000
|
||||
onTriggered: {
|
||||
root.reset();
|
||||
}
|
||||
}
|
||||
|
||||
onCurrentTextChanged: {
|
||||
if (currentText.length > 0) {
|
||||
showFailure = false;
|
||||
GlobalStates.screenUnlockFailed = false;
|
||||
}
|
||||
GlobalStates.screenLockContainsCharacters = currentText.length > 0;
|
||||
passwordClearTimer.restart();
|
||||
}
|
||||
|
||||
function tryUnlock(alsoInhibitIdle = false) {
|
||||
root.alsoInhibitIdle = alsoInhibitIdle;
|
||||
root.unlockInProgress = true;
|
||||
pam.start();
|
||||
}
|
||||
|
||||
function tryFingerUnlock() {
|
||||
if (root.fingerprintsConfigured) {
|
||||
fingerPam.start();
|
||||
}
|
||||
}
|
||||
|
||||
function stopFingerPam() {
|
||||
if (fingerPam.active) {
|
||||
fingerPam.abort();
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: fingerprintCheckProc
|
||||
running: true
|
||||
command: ["bash", "-c", "fprintd-list $(whoami)"]
|
||||
stdout: StdioCollector {
|
||||
id: fingerprintOutputCollector
|
||||
onStreamFinished: {
|
||||
root.fingerprintsConfigured = fingerprintOutputCollector.text.includes("Fingerprints for user");
|
||||
}
|
||||
}
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode !== 0) {
|
||||
// console.warn("[LockContext] fprintd-list command exited with error:", exitCode, exitStatus);
|
||||
root.fingerprintsConfigured = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: pam
|
||||
|
||||
// pam_unix will ask for a response for the password prompt
|
||||
onPamMessage: {
|
||||
if (this.responseRequired) {
|
||||
this.respond(root.currentText);
|
||||
}
|
||||
}
|
||||
|
||||
// pam_unix won't send any important messages so all we need is the completion status.
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked(root.targetAction);
|
||||
stopFingerPam();
|
||||
} else {
|
||||
root.clearText();
|
||||
root.unlockInProgress = false;
|
||||
GlobalStates.screenUnlockFailed = true;
|
||||
root.showFailure = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PamContext {
|
||||
id: fingerPam
|
||||
|
||||
configDirectory: "pam"
|
||||
config: "fprintd.conf"
|
||||
|
||||
onCompleted: result => {
|
||||
if (result == PamResult.Success) {
|
||||
root.unlocked(root.targetAction);
|
||||
stopFingerPam();
|
||||
} else if (result == PamResult.Error) { // if timeout or etc..
|
||||
tryFingerUnlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,163 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
|
||||
required property Component lockSurface
|
||||
property alias context: lockContext
|
||||
property Component sessionLockSurface: WlSessionLockSurface {
|
||||
id: sessionLockSurface
|
||||
color: "transparent"
|
||||
Loader {
|
||||
active: GlobalStates.screenLocked
|
||||
anchors.fill: parent
|
||||
opacity: active ? 1 : 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
sourceComponent: root.lockSurface
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: unlockKeyringProc
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
KeyringStorage.fetchKeyringData();
|
||||
}
|
||||
}
|
||||
function unlockKeyring() {
|
||||
unlockKeyringProc.exec({
|
||||
environment: ({
|
||||
"UNLOCK_PASSWORD": lockContext.currentText
|
||||
}),
|
||||
command: ["bash", "-c", Quickshell.shellPath("scripts/keyring/unlock.sh")]
|
||||
})
|
||||
}
|
||||
|
||||
// This stores all the information shared between the lock surfaces on each screen.
|
||||
// https://github.com/quickshell-mirror/quickshell-examples/tree/master/lockscreen
|
||||
LockContext {
|
||||
id: lockContext
|
||||
|
||||
Connections {
|
||||
target: GlobalStates
|
||||
function onScreenLockedChanged() {
|
||||
if (GlobalStates.screenLocked) {
|
||||
lockContext.reset();
|
||||
lockContext.tryFingerUnlock();
|
||||
|
||||
// Отключаем экран по таймауту, если параметр больше нуля
|
||||
if (Config.options.lock.dpmsTimeout > 0) {
|
||||
Quickshell.execDetached(["bash", "-c", `sleep ${Config.options.lock.dpmsTimeout} && hyprctl dispatch dpms off`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onUnlocked: (targetAction) => {
|
||||
// Perform the target action if it's not just unlocking
|
||||
if (targetAction == LockContext.ActionEnum.Poweroff) {
|
||||
Session.poweroff();
|
||||
return;
|
||||
} else if (targetAction == LockContext.ActionEnum.Reboot) {
|
||||
Session.reboot();
|
||||
return;
|
||||
}
|
||||
|
||||
// Unlock the keyring if configured to do so
|
||||
if (Config.options.lock.security.unlockKeyring) root.unlockKeyring(); // Async
|
||||
|
||||
// Выполняем пользовательский хук разблокировки, если он задан
|
||||
if (Config.options.lock.unlockHook && Config.options.lock.unlockHook !== "") {
|
||||
Quickshell.execDetached(["bash", "-c", Config.options.lock.unlockHook]);
|
||||
}
|
||||
|
||||
// Unlock the screen before exiting, or the compositor will display a
|
||||
// fallback lock you can't interact with.
|
||||
GlobalStates.screenLocked = false;
|
||||
|
||||
// Reset
|
||||
lockContext.reset();
|
||||
|
||||
// Post-unlock actions
|
||||
if (lockContext.alsoInhibitIdle) {
|
||||
lockContext.alsoInhibitIdle = false;
|
||||
Idle.toggleInhibit(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WlSessionLock {
|
||||
id: lock
|
||||
locked: GlobalStates.screenLocked
|
||||
surface: root.sessionLockSurface
|
||||
}
|
||||
|
||||
function lock() {
|
||||
if (Config.options.lock.useHyprlock) {
|
||||
Quickshell.execDetached(["bash", "-c", "pidof hyprlock || hyprlock"]);
|
||||
return;
|
||||
}
|
||||
GlobalStates.screenLocked = true;
|
||||
}
|
||||
|
||||
IpcHandler {
|
||||
target: "lock"
|
||||
|
||||
function activate(): void {
|
||||
root.lock();
|
||||
}
|
||||
function focus(): void {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lock"
|
||||
description: "Locks the screen"
|
||||
|
||||
onPressed: {
|
||||
root.lock()
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "lockFocus"
|
||||
description: "Re-focuses the lock screen. This is because Hyprland after waking up for whatever reason"
|
||||
+ "decides to keyboard-unfocus the lock screen"
|
||||
|
||||
onPressed: {
|
||||
lockContext.shouldReFocus();
|
||||
}
|
||||
}
|
||||
|
||||
function initIfReady() {
|
||||
if (!Config.ready || !Persistent.ready) return;
|
||||
if (Config.options.lock.launchOnStartup && Persistent.isNewHyprlandInstance) {
|
||||
root.lock();
|
||||
} else {
|
||||
KeyringStorage.fetchKeyringData();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Config
|
||||
function onReadyChanged() {
|
||||
root.initIfReady();
|
||||
}
|
||||
}
|
||||
Connections {
|
||||
target: Persistent
|
||||
function onReadyChanged() {
|
||||
root.initIfReady();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
auth sufficient pam_fprintd.so
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
Process {
|
||||
id: root
|
||||
|
||||
signal done(string path, int width, int height);
|
||||
required property string filePath;
|
||||
required property string sourceUrl;
|
||||
property string downloadUserAgent: Config.options?.networking.userAgent ?? ""
|
||||
|
||||
function processFilePath() {
|
||||
return StringUtils.shellSingleQuoteEscape(FileUtils.trimFileProtocol(filePath));
|
||||
}
|
||||
|
||||
function processSourceUrl() {
|
||||
return StringUtils.shellSingleQuoteEscape(sourceUrl);
|
||||
}
|
||||
|
||||
function curlUserAgentArg() {
|
||||
if (!downloadUserAgent) {
|
||||
return "";
|
||||
}
|
||||
return ` -H 'User-Agent: ${StringUtils.shellSingleQuoteEscape(downloadUserAgent)}'`;
|
||||
}
|
||||
|
||||
running: true
|
||||
command: ["bash", "-c",
|
||||
`mkdir -p $(dirname '${processFilePath()}'); [ -f '${processFilePath()}' ] || curl -sSL '${processSourceUrl()}'${curlUserAgentArg()} -o '${processFilePath()}' && file '${processFilePath()}'`
|
||||
]
|
||||
stdout: StdioCollector {
|
||||
id: imageSizeOutputCollector
|
||||
onStreamFinished: {
|
||||
const output = imageSizeOutputCollector.text.trim();
|
||||
const match = output.match(/(\d+)\s*x\s*(\d+)/);
|
||||
|
||||
if (match) {
|
||||
const width = Number(match[1]);
|
||||
const height = Number(match[2]);
|
||||
root.done(root.filePath, width, height);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
|
||||
Process {
|
||||
id: proc
|
||||
|
||||
signal finished(string output)
|
||||
|
||||
property list<var> sequence: []
|
||||
property int index: 0
|
||||
|
||||
// Runs a sequence of command and functions
|
||||
function runSequence(seq) {
|
||||
if (seq.length == 0)
|
||||
return;
|
||||
sequence = seq;
|
||||
running = false;
|
||||
index = -1;
|
||||
step();
|
||||
}
|
||||
|
||||
function step(output) {
|
||||
index++;
|
||||
if (index >= sequence.length) {
|
||||
finished(output);
|
||||
return;
|
||||
}
|
||||
const nextItem = sequence[index];
|
||||
if (typeof nextItem === "function") {
|
||||
const nextOutput = nextItem(output);
|
||||
step(nextOutput);
|
||||
} else {
|
||||
// If empty command then not set; this allows setting it with previous function
|
||||
if (nextItem.length > 0)
|
||||
command = nextItem;
|
||||
running = true;
|
||||
}
|
||||
}
|
||||
|
||||
stdout: StdioCollector {
|
||||
onStreamFinished: {
|
||||
proc.step(text);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
pragma Singleton
|
||||
import qs.modules.common
|
||||
import qs.modules.common.utils
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt.labs.synchronizer
|
||||
import Quickshell
|
||||
|
||||
Singleton {
|
||||
id: root
|
||||
|
||||
enum Action {
|
||||
Copy,
|
||||
Edit,
|
||||
Search,
|
||||
CharRecognition,
|
||||
Record,
|
||||
RecordWithSound
|
||||
}
|
||||
|
||||
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
|
||||
property string fileUploadApiEndpoint: "https://uguu.se/upload"
|
||||
|
||||
function getCommand(x, y, width, height, screenshotPath, action, saveDir = "") {
|
||||
// Set command for action
|
||||
const rx = Math.round(x);
|
||||
const ry = Math.round(y);
|
||||
const rw = Math.round(width);
|
||||
const rh = Math.round(height);
|
||||
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(screenshotPath)} `
|
||||
+ `-crop ${rw}x${rh}+${rx}+${ry} +repage`
|
||||
const cropToStdout = `${cropBase} -`
|
||||
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
|
||||
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`
|
||||
const slurpRegion = `${rx},${ry} ${rw}x${rh}`
|
||||
const uploadAndGetUrl = (filePath) => {
|
||||
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
|
||||
}
|
||||
const annotationCommand = `${Config.options.regionSelector.annotation.useSatty ? "satty" : "swappy"} -f -`;
|
||||
switch (action) {
|
||||
case ScreenshotAction.Action.Copy:
|
||||
if (saveDir === "") {
|
||||
// not saving the screenshot, just copy to clipboard
|
||||
return ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
|
||||
break;
|
||||
}
|
||||
return [
|
||||
"bash", "-c",
|
||||
`mkdir -p '${StringUtils.shellSingleQuoteEscape(saveDir)}' && \
|
||||
saveFileName="screenshot-$(date '+%Y-%m-%d_%H.%M.%S').png" && \
|
||||
savePath="${saveDir}/$saveFileName" && \
|
||||
${cropToStdout} | tee >(wl-copy) > "$savePath" && \
|
||||
${cleanup}`
|
||||
]
|
||||
|
||||
break;
|
||||
case ScreenshotAction.Action.Edit:
|
||||
return ["bash", "-c", `${cropToStdout} | ${annotationCommand} && ${cleanup}`]
|
||||
break;
|
||||
case ScreenshotAction.Action.Search:
|
||||
return ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(screenshotPath)})" && ${cleanup}`]
|
||||
break;
|
||||
case ScreenshotAction.Action.CharRecognition:
|
||||
return ["bash", "-c", `${cropInPlace} && tesseract '${StringUtils.shellSingleQuoteEscape(screenshotPath)}' stdout -l $(tesseract --list-langs | awk 'NR>1{print $1}' | tr '\\n' '+' | sed 's/\\+$/\\n/') | wl-copy && ${cleanup}`]
|
||||
break;
|
||||
case ScreenshotAction.Action.Record:
|
||||
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}'`]
|
||||
break;
|
||||
case ScreenshotAction.Action.RecordWithSound:
|
||||
return ["bash", "-c", `${Directories.recordScriptPath} --region '${slurpRegion}' --sound`]
|
||||
break;
|
||||
default:
|
||||
console.warn("[Region Selector] Unknown snip action, skipping snip.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
Process {
|
||||
id: screenshotProc
|
||||
running: true
|
||||
property string screenshotDir: Directories.screenshotTemp
|
||||
required property ShellScreen screen
|
||||
property string screenshotPath: `${screenshotDir}/image-${screen.name}`
|
||||
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(screen.name)}' '${StringUtils.shellSingleQuoteEscape(screenshotPath)}'`]
|
||||
}
|
||||
|
|
@ -0,0 +1,120 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
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
|
||||
downAction: () => 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
|
||||
downAction: () => root.showBreadcrumb = !root.showBreadcrumb
|
||||
contentItem: MaterialSymbol {
|
||||
text: "edit"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
color: dirEditButton.toggled ? Appearance.colors.colOnPrimary : Appearance.colors.colOnLayer2
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: 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("/"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,49 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
/**
|
||||
* A container that supports GroupButton children for bounciness.
|
||||
* See https://m3.material.io/components/button-groups/overview
|
||||
*/
|
||||
Rectangle {
|
||||
id: root
|
||||
default property alias groupData: rowLayout.data
|
||||
property alias uniformCellSizes: rowLayout.uniformCellSizes
|
||||
property real spacing: 5
|
||||
property real padding: 0
|
||||
property alias clickIndex: rowLayout.clickIndex
|
||||
property alias childrenCount: rowLayout.childrenCount
|
||||
|
||||
property real contentWidth: {
|
||||
let total = 0;
|
||||
for (let i = 0; i < rowLayout.children.length; ++i) {
|
||||
const child = rowLayout.children[i];
|
||||
if (!child.visible) continue;
|
||||
total += child.baseWidth ?? child.implicitWidth ?? child.width;
|
||||
}
|
||||
return total + rowLayout.spacing * (rowLayout.children.length - 1);
|
||||
}
|
||||
|
||||
topLeftRadius: rowLayout.children.length > 0 ? (rowLayout.children[0].radius + padding) :
|
||||
Appearance?.rounding?.small
|
||||
bottomLeftRadius: topLeftRadius
|
||||
topRightRadius: rowLayout.children.length > 0 ? (rowLayout.children[rowLayout.children.length - 1].radius + padding) :
|
||||
Appearance?.rounding?.small
|
||||
bottomRightRadius: topRightRadius
|
||||
|
||||
color: "transparent"
|
||||
width: root.contentWidth + padding * 2
|
||||
implicitHeight: rowLayout.implicitHeight + padding * 2
|
||||
implicitWidth: root.contentWidth + padding * 2
|
||||
|
||||
children: [RowLayout {
|
||||
id: rowLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: root.padding
|
||||
spacing: root.spacing
|
||||
property int clickIndex: -1
|
||||
property int childrenCount: children.length
|
||||
}]
|
||||
}
|
||||
|
|
@ -0,0 +1,123 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQml
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.waffle.looks
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
// Expose delegate
|
||||
property Component delegate: Text {
|
||||
required property var model
|
||||
text: model.day
|
||||
}
|
||||
|
||||
// Configuration
|
||||
property int paddingWeeks: 2 // 1 should be sufficient with proper clipping and no padding
|
||||
property var locale: Qt.locale() // Should be of type Locale but QML is being funny
|
||||
|
||||
// Scrolling
|
||||
function scrollMonthsAndSnap(x) { // Scroll x months and snap to month
|
||||
const focusedDate = root.focusedDate;
|
||||
const focusedMonth = focusedDate.getMonth();
|
||||
const focusedYear = focusedDate.getFullYear();
|
||||
const targetMonth = focusedMonth + x;
|
||||
const targetDate = new Date(focusedYear, targetMonth, 1);
|
||||
const currentFirstShownDate = new Date(root.dateInFirstWeek.getTime() + (root.paddingWeeks * root.millisPerWeek));
|
||||
const diffMillis = targetDate.getTime() - currentFirstShownDate.getTime();
|
||||
const diffWeeks = Math.round(diffMillis / root.millisPerWeek);
|
||||
root.targetWeekDiff += diffWeeks;
|
||||
}
|
||||
property int weeksPerScroll: 1
|
||||
property real targetWeekDiff: 0
|
||||
property real weekDiff: targetWeekDiff
|
||||
property int contentWeekDiff: weekDiff // whole part of weekDiff
|
||||
property bool scrolling: false
|
||||
|
||||
Behavior on weekDiff {
|
||||
id: weekScrollBehavior
|
||||
animation: Looks.transition.scroll.createObject(this)
|
||||
}
|
||||
Timer {
|
||||
id: scrollAnimationCheckTimer
|
||||
interval: 30 // Should be plenty for 60fps
|
||||
onTriggered: root.scrolling = false;
|
||||
}
|
||||
onWeekDiffChanged: {
|
||||
scrolling = true;
|
||||
scrollAnimationCheckTimer.restart();
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onWheel: wheel => {
|
||||
root.targetWeekDiff += wheel.angleDelta.y / 120 * -root.weeksPerScroll; // Reverse cuz scrolling down should advance
|
||||
}
|
||||
}
|
||||
|
||||
// Date calculations
|
||||
readonly property int millisPerWeek: 7 * 24 * 60 * 60 * 1000
|
||||
readonly property int totalWeeks: 6 + (paddingWeeks * 2)
|
||||
readonly property int focusedWeekIndex: 2 // The third row, 0-indexed
|
||||
readonly property int focusDayOfWeekIndex: 6
|
||||
property date dateInFirstWeek: {
|
||||
const currentDate = new Date();
|
||||
const currentMonth = currentDate.getMonth();
|
||||
const currentYear = currentDate.getFullYear();
|
||||
const firstDayThisMonth = new Date(currentYear, currentMonth, 1);
|
||||
return new Date(firstDayThisMonth.getTime() - (paddingWeeks * millisPerWeek) + contentWeekDiff * millisPerWeek);
|
||||
}
|
||||
property date focusedDate: {
|
||||
// The last day of 3rd week shown is considered the focused month
|
||||
const addedTime = (root.paddingWeeks + root.focusedWeekIndex) * root.millisPerWeek
|
||||
const dateInTargetWeek = new Date(root.dateInFirstWeek.getTime() + addedTime);
|
||||
return DateUtils.getIthDayDateOfSameWeek(dateInTargetWeek, root.focusDayOfWeekIndex - root.locale.firstDayOfWeek, root.locale.firstdayOfWeek); // 4 = Thursday
|
||||
}
|
||||
property int focusedMonth: focusedDate.getMonth() + 1 // 0-indexed -> 1-indexed
|
||||
|
||||
// Sizes
|
||||
property real verticalPadding: 0
|
||||
property real buttonSize: 40
|
||||
property real buttonSpacing: 2
|
||||
property real buttonVerticalSpacing: buttonSpacing
|
||||
implicitHeight: (6 * buttonSize) + (5 * buttonVerticalSpacing) + (2 * verticalPadding)
|
||||
implicitWidth: weeksColumn.implicitWidth
|
||||
clip: true
|
||||
|
||||
ColumnLayout {
|
||||
id: weeksColumn
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
y: {
|
||||
const spacePerExtraRow = root.buttonSize + root.buttonVerticalSpacing;
|
||||
const origin = -(spacePerExtraRow * root.paddingWeeks);
|
||||
const diff = root.weekDiff * spacePerExtraRow;
|
||||
return origin + (-diff % spacePerExtraRow) + root.verticalPadding;
|
||||
}
|
||||
|
||||
spacing: root.buttonVerticalSpacing
|
||||
|
||||
Repeater {
|
||||
model: root.totalWeeks
|
||||
|
||||
WeekRow {
|
||||
required property int index
|
||||
locale: root.locale
|
||||
date: new Date(root.dateInFirstWeek.getTime() + (index * root.millisPerWeek))
|
||||
Layout.fillWidth: true
|
||||
spacing: root.buttonSpacing
|
||||
delegate: root.delegate
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
property double diameter
|
||||
|
||||
implicitWidth: diameter
|
||||
implicitHeight: diameter
|
||||
radius: diameter / 2
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import qs.modules.common
|
||||
|
||||
/**
|
||||
* Material 3 circular progress. See https://m3.material.io/components/progress-indicators/specs
|
||||
*/
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int implicitSize: 30
|
||||
property int lineWidth: 2
|
||||
property real value: 0
|
||||
property color colPrimary: Appearance.m3colors.m3onSecondaryContainer
|
||||
property color colSecondary: Appearance.colors.colSecondaryContainer
|
||||
property real gapAngle: 360 / 18
|
||||
property bool fill: false
|
||||
property int fillOverflow: 2
|
||||
property bool enableAnimation: true
|
||||
property int animationDuration: 800
|
||||
property var easingType: Easing.OutCubic
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property real degree: value * 360
|
||||
property real centerX: root.width / 2
|
||||
property real centerY: root.height / 2
|
||||
property real arcRadius: root.implicitSize / 2 - root.lineWidth
|
||||
property real startAngle: -90
|
||||
|
||||
Behavior on degree {
|
||||
enabled: root.enableAnimation
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.easingType
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.fill
|
||||
anchors.fill: parent
|
||||
|
||||
sourceComponent: Rectangle {
|
||||
radius: 9999
|
||||
color: root.colSecondary
|
||||
}
|
||||
}
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
ShapePath {
|
||||
id: secondaryPath
|
||||
strokeColor: root.colSecondary
|
||||
strokeWidth: root.lineWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
PathAngleArc {
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle - root.gapAngle
|
||||
sweepAngle: -(360 - root.degree - 2 * root.gapAngle)
|
||||
}
|
||||
}
|
||||
ShapePath {
|
||||
id: primaryPath
|
||||
strokeColor: root.colPrimary
|
||||
strokeWidth: root.lineWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: "transparent"
|
||||
PathAngleArc {
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle
|
||||
sweepAngle: root.degree
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,129 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property string entry
|
||||
property real maxWidth
|
||||
property real maxHeight
|
||||
property bool blur: false
|
||||
property string blurText: "Image hidden"
|
||||
|
||||
property string imageDecodePath: Directories.cliphistDecode
|
||||
property string imageDecodeFileName: `${entryNumber}`
|
||||
property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}`
|
||||
property string source
|
||||
|
||||
property int entryNumber: {
|
||||
if (!root.entry)
|
||||
return 0;
|
||||
const match = root.entry.match(/^(\d+)\t/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}
|
||||
property int imageWidth: {
|
||||
if (!root.entry)
|
||||
return 0;
|
||||
const match = root.entry.match(/(\d+)x(\d+)/);
|
||||
return match ? parseInt(match[1]) : 0;
|
||||
}
|
||||
property int imageHeight: {
|
||||
if (!root.entry)
|
||||
return 0;
|
||||
const match = root.entry.match(/(\d+)x(\d+)/);
|
||||
return match ? parseInt(match[2]) : 0;
|
||||
}
|
||||
property real scale: {
|
||||
return Math.min(root.maxWidth / imageWidth, root.maxHeight / imageHeight, 1);
|
||||
}
|
||||
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: Appearance.rounding.small
|
||||
implicitHeight: imageHeight * scale
|
||||
implicitWidth: imageWidth * scale
|
||||
|
||||
Component.onCompleted: {
|
||||
decodeImageProcess.running = true;
|
||||
}
|
||||
|
||||
Process {
|
||||
id: decodeImageProcess
|
||||
command: ["bash", "-c", `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
if (exitCode === 0) {
|
||||
root.source = imageDecodeFilePath;
|
||||
} else {
|
||||
console.error("[CliphistImage] Failed to decode image for entry:", root.entry);
|
||||
root.source = "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Component.onDestruction: {
|
||||
Quickshell.execDetached(["bash", "-c", `[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'`]);
|
||||
}
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: image.width
|
||||
height: image.height
|
||||
radius: root.radius
|
||||
}
|
||||
}
|
||||
|
||||
StyledImage {
|
||||
id: image
|
||||
anchors.fill: parent
|
||||
|
||||
source: Qt.resolvedUrl(root.source)
|
||||
fillMode: Image.PreserveAspectFit
|
||||
antialiasing: true
|
||||
asynchronous: true
|
||||
|
||||
width: root.imageWidth * root.scale
|
||||
height: root.imageHeight * root.scale
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: blurLoader
|
||||
active: root.blur
|
||||
anchors.fill: image
|
||||
sourceComponent: GaussianBlur {
|
||||
source: image
|
||||
radius: 35
|
||||
samples: radius * 2 + 1
|
||||
|
||||
Rectangle {
|
||||
anchors.fill: parent
|
||||
color: ColorUtils.transparentize(Appearance.colors.colLayer0, 0.5)
|
||||
|
||||
Column {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: width <= image.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: "visibility_off"
|
||||
font.pixelSize: 28
|
||||
}
|
||||
StyledText {
|
||||
visible: width <= image.width
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
text: root.blurText
|
||||
color: Appearance.colors.colOnSurface
|
||||
font.pixelSize: Appearance.font.pixelSize.smallie
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property int implicitSize: 18
|
||||
property int lineWidth: 2
|
||||
property real value: 0
|
||||
property color colPrimary: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
|
||||
property color colSecondary: ColorUtils.transparentize(colPrimary, 0.5) ?? "#F1D3F9"
|
||||
property real gapAngle: 360 / 18
|
||||
property bool fill: true
|
||||
property int fillOverflow: 2
|
||||
property bool enableAnimation: true
|
||||
property int animationDuration: 800
|
||||
property var easingType: Easing.OutCubic
|
||||
property bool accountForLightBleeding: true
|
||||
default property Item textMask: Item {
|
||||
width: implicitSize
|
||||
height: implicitSize
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
text: Math.round(root.value * 100)
|
||||
font.pixelSize: 12
|
||||
font.weight: Font.Medium
|
||||
}
|
||||
}
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property real degree: value * 360
|
||||
property real centerX: root.width / 2
|
||||
property real centerY: root.height / 2
|
||||
property real arcRadius: root.implicitSize / 2 - root.lineWidth / 2 - (0.5 * root.accountForLightBleeding)
|
||||
property real startAngle: -90
|
||||
|
||||
Behavior on degree {
|
||||
enabled: root.enableAnimation
|
||||
NumberAnimation {
|
||||
duration: root.animationDuration
|
||||
easing.type: root.easingType
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
radius: implicitSize / 2
|
||||
color: root.colSecondary
|
||||
visible: false
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
|
||||
Shape {
|
||||
anchors.fill: parent
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: primaryPath
|
||||
pathHints: ShapePath.PathSolid & ShapePath.PathNonIntersecting
|
||||
strokeColor: root.colPrimary
|
||||
strokeWidth: root.lineWidth
|
||||
capStyle: ShapePath.RoundCap
|
||||
fillColor: root.colPrimary
|
||||
|
||||
startX: root.centerX
|
||||
startY: root.centerY
|
||||
|
||||
PathAngleArc {
|
||||
moveToStart: false
|
||||
centerX: root.centerX
|
||||
centerY: root.centerY
|
||||
radiusX: root.arcRadius
|
||||
radiusY: root.arcRadius
|
||||
startAngle: root.startAngle
|
||||
sweepAngle: root.degree
|
||||
}
|
||||
PathLine {
|
||||
x: primaryPath.startX
|
||||
y: primaryPath.startY
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
anchors.fill: parent
|
||||
source: contentItem
|
||||
invert: true
|
||||
maskSource: root.textMask
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,101 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
/**
|
||||
* A progress bar with both ends rounded and text acts as clipping like OneUI 7's battery indicator.
|
||||
*/
|
||||
ProgressBar {
|
||||
id: root
|
||||
property bool vertical: false
|
||||
property real valueBarWidth: 30
|
||||
property real valueBarHeight: 18
|
||||
property color highlightColor: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
|
||||
property color trackColor: ColorUtils.transparentize(highlightColor, 0.5) ?? "#F1D3F9"
|
||||
property alias radius: contentItem.radius
|
||||
property string text
|
||||
default property Item textMask: Item {
|
||||
width: valueBarWidth
|
||||
height: valueBarHeight
|
||||
StyledText {
|
||||
anchors.centerIn: parent
|
||||
font: root.font
|
||||
text: root.text
|
||||
}
|
||||
}
|
||||
|
||||
text: Math.round(value * 100)
|
||||
font {
|
||||
pixelSize: 13
|
||||
weight: text.length > 2 ? Font.Medium : Font.DemiBold
|
||||
}
|
||||
|
||||
background: Item {
|
||||
implicitHeight: valueBarHeight
|
||||
implicitWidth: valueBarWidth
|
||||
}
|
||||
|
||||
contentItem: Rectangle {
|
||||
id: contentItem
|
||||
anchors.fill: parent
|
||||
radius: 9999
|
||||
color: root.trackColor
|
||||
visible: false
|
||||
|
||||
Rectangle {
|
||||
id: progressFill
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: undefined
|
||||
}
|
||||
width: parent.width * root.visualPosition
|
||||
height: parent.height
|
||||
|
||||
states: State {
|
||||
name: "vertical"
|
||||
when: root.vertical
|
||||
AnchorChanges {
|
||||
target: progressFill
|
||||
anchors {
|
||||
top: undefined
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
PropertyChanges {
|
||||
target: progressFill
|
||||
width: parent.width
|
||||
height: parent.height * root.visualPosition
|
||||
}
|
||||
}
|
||||
|
||||
radius: Appearance.rounding.unsharpen
|
||||
color: root.highlightColor
|
||||
}
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
id: roundingMask
|
||||
visible: false
|
||||
anchors.fill: parent
|
||||
source: contentItem
|
||||
maskSource: Rectangle {
|
||||
width: contentItem.width
|
||||
height: contentItem.height
|
||||
radius: contentItem.radius
|
||||
}
|
||||
}
|
||||
|
||||
OpacityMask {
|
||||
anchors.fill: parent
|
||||
source: roundingMask
|
||||
invert: true
|
||||
maskSource: root.textMask
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
property bool uniform: false
|
||||
spacing: 4
|
||||
uniformCellSizes: uniform
|
||||
}
|
||||
|
|
@ -0,0 +1,54 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
|
||||
Flow {
|
||||
id: root
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
property list<var> options: [
|
||||
{
|
||||
"displayName": "Option 1",
|
||||
"icon": "check",
|
||||
"value": 1
|
||||
},
|
||||
{
|
||||
"displayName": "Option 2",
|
||||
"icon": "close",
|
||||
"value": 2
|
||||
},
|
||||
]
|
||||
property var currentValue: null
|
||||
|
||||
signal selected(var newValue)
|
||||
|
||||
Repeater {
|
||||
model: root.options
|
||||
delegate: SelectionGroupButton {
|
||||
id: paletteButton
|
||||
required property var modelData
|
||||
required property int index
|
||||
onYChanged: {
|
||||
if (index === 0) {
|
||||
paletteButton.leftmost = true
|
||||
} else {
|
||||
var prev = root.children[index - 1]
|
||||
var thisIsOnNewLine = prev && prev.y !== paletteButton.y
|
||||
paletteButton.leftmost = thisIsOnNewLine
|
||||
prev.rightmost = thisIsOnNewLine
|
||||
}
|
||||
}
|
||||
leftmost: index === 0
|
||||
rightmost: index === root.options.length - 1
|
||||
buttonIcon: modelData.icon || ""
|
||||
buttonText: modelData.displayName
|
||||
toggled: root.currentValue == modelData.value
|
||||
onClicked: {
|
||||
root.selected(modelData.value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
import qs.modules.common.widgets
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.services
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
spacing: 10
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
|
||||
property string text: ""
|
||||
property string buttonIcon: ""
|
||||
property alias value: slider.value
|
||||
property alias stopIndicatorValues: slider.stopIndicatorValues
|
||||
property bool usePercentTooltip: true
|
||||
property real from: slider.from
|
||||
property real to: slider.to
|
||||
property real textWidth: 120
|
||||
|
||||
RowLayout {
|
||||
id: row
|
||||
spacing: 10
|
||||
|
||||
OptionalMaterialSymbol {
|
||||
id: iconWidget
|
||||
icon: root.buttonIcon
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.preferredWidth: root.textWidth
|
||||
text: root.text
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
StyledSlider {
|
||||
id: slider
|
||||
configuration: StyledSlider.Configuration.XS
|
||||
usePercentTooltip: root.usePercentTooltip
|
||||
value: root.value
|
||||
from: root.from
|
||||
to: root.to
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
import qs.modules.common.widgets
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
RowLayout {
|
||||
id: root
|
||||
property string text: ""
|
||||
property string icon
|
||||
property alias value: spinBoxWidget.value
|
||||
property alias stepSize: spinBoxWidget.stepSize
|
||||
property alias from: spinBoxWidget.from
|
||||
property alias to: spinBoxWidget.to
|
||||
spacing: 10
|
||||
Layout.leftMargin: 8
|
||||
Layout.rightMargin: 8
|
||||
|
||||
RowLayout {
|
||||
spacing: 10
|
||||
OptionalMaterialSymbol {
|
||||
icon: root.icon
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
}
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
}
|
||||
}
|
||||
|
||||
StyledSpinBox {
|
||||
id: spinBoxWidget
|
||||
Layout.fillWidth: false
|
||||
value: root.value
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import qs.modules.common.widgets
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Controls
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property string buttonIcon
|
||||
property alias iconSize: iconWidget.iconSize
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentItem.implicitHeight + 8 * 2
|
||||
font.pixelSize: Appearance.font.pixelSize.small
|
||||
|
||||
onClicked: checked = !checked
|
||||
|
||||
contentItem: RowLayout {
|
||||
spacing: 10
|
||||
OptionalMaterialSymbol {
|
||||
id: iconWidget
|
||||
icon: root.buttonIcon
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
StyledText {
|
||||
id: labelWidget
|
||||
Layout.fillWidth: true
|
||||
text: root.text
|
||||
font: root.font
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
}
|
||||
StyledSwitch {
|
||||
id: switchWidget
|
||||
down: root.down
|
||||
Layout.fillWidth: false
|
||||
checked: root.checked
|
||||
onClicked: root.clicked()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledFlickable {
|
||||
id: root
|
||||
property real baseWidth: 600
|
||||
property bool forceWidth: false
|
||||
property real bottomContentPadding: 100
|
||||
|
||||
default property alias contentData: contentColumn.data
|
||||
|
||||
clip: true
|
||||
contentHeight: contentColumn.implicitHeight + root.bottomContentPadding // Add some padding at the bottom
|
||||
implicitWidth: contentColumn.implicitWidth
|
||||
|
||||
ColumnLayout {
|
||||
id: contentColumn
|
||||
width: root.forceWidth ? root.baseWidth : Math.max(root.baseWidth, implicitWidth)
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
margins: 20
|
||||
}
|
||||
spacing: 30
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,36 @@
|
|||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property string title
|
||||
property string icon: ""
|
||||
default property alias contentData: sectionContent.data
|
||||
|
||||
Layout.fillWidth: true
|
||||
spacing: 6
|
||||
|
||||
RowLayout {
|
||||
spacing: 6
|
||||
OptionalMaterialSymbol {
|
||||
icon: root.icon
|
||||
iconSize: Appearance.font.pixelSize.hugeass
|
||||
}
|
||||
StyledText {
|
||||
text: root.title
|
||||
font.pixelSize: Appearance.font.pixelSize.larger
|
||||
font.weight: Font.Medium
|
||||
color: Appearance.colors.colOnSecondaryContainer
|
||||
}
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: sectionContent
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
property string title: ""
|
||||
property string tooltip: ""
|
||||
default property alias contentData: sectionContent.data
|
||||
|
||||
Layout.fillWidth: true
|
||||
Layout.topMargin: 4
|
||||
spacing: 2
|
||||
|
||||
RowLayout {
|
||||
ContentSubsectionLabel {
|
||||
visible: root.title && root.title.length > 0
|
||||
text: root.title
|
||||
}
|
||||
MaterialSymbol {
|
||||
visible: root.tooltip && root.tooltip.length > 0
|
||||
text: "info"
|
||||
iconSize: Appearance.font.pixelSize.large
|
||||
|
||||
color: Appearance.colors.colSubtext
|
||||
MouseArea {
|
||||
id: infoMouseArea
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
cursorShape: Qt.WhatsThisCursor
|
||||
StyledToolTip {
|
||||
extraVisibleCondition: false
|
||||
alternativeVisibleCondition: infoMouseArea.containsMouse
|
||||
text: root.tooltip
|
||||
}
|
||||
}
|
||||
}
|
||||
Item { Layout.fillWidth: true }
|
||||
}
|
||||
ColumnLayout {
|
||||
id: sectionContent
|
||||
Layout.fillWidth: true
|
||||
spacing: 2
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
StyledText {
|
||||
text: "Subsection"
|
||||
color: Appearance.colors.colSubtext
|
||||
Layout.leftMargin: 2
|
||||
}
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
import Qt5Compat.GraphicalEffects
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
property bool colorize: false
|
||||
property color color
|
||||
property string source: ""
|
||||
property string iconFolder: Qt.resolvedUrl(Quickshell.shellPath("assets/icons")) // The folder to check first
|
||||
width: 30
|
||||
height: 30
|
||||
|
||||
IconImage {
|
||||
id: iconImage
|
||||
anchors.fill: parent
|
||||
source: {
|
||||
const fullPathWhenSourceIsIconName = iconFolder + "/" + root.source;
|
||||
if (iconFolder && fullPathWhenSourceIsIconName) {
|
||||
return fullPathWhenSourceIsIconName
|
||||
}
|
||||
return root.source
|
||||
}
|
||||
implicitSize: root.height
|
||||
}
|
||||
|
||||
Loader {
|
||||
active: root.colorize
|
||||
anchors.fill: iconImage
|
||||
sourceComponent: ColorOverlay {
|
||||
source: iconImage
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
Canvas {
|
||||
id: root
|
||||
property color color: "#ffffff"
|
||||
property int dashLength: 6
|
||||
property int gapLength: 4
|
||||
property int borderWidth: 1
|
||||
|
||||
onDashLengthChanged: requestPaint()
|
||||
onGapLengthChanged: requestPaint()
|
||||
onWidthChanged: requestPaint()
|
||||
onHeightChanged: requestPaint()
|
||||
onPaint: {
|
||||
var ctx = getContext("2d");
|
||||
ctx.clearRect(0, 0, width, height);
|
||||
ctx.save();
|
||||
ctx.strokeStyle = root.color;
|
||||
ctx.lineWidth = root.borderWidth;
|
||||
if (root.gapLength > 0) {
|
||||
ctx.setLineDash([root.dashLength, root.gapLength]); // Set dash pattern
|
||||
}
|
||||
ctx.strokeRect(root.borderWidth / 2, root.borderWidth / 2, width - root.borderWidth, height - root.borderWidth); // Draw it
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,40 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* Material 3 dialog button. See https://m3.material.io/components/dialogs/overview
|
||||
*/
|
||||
RippleButton {
|
||||
id: root
|
||||
|
||||
property string buttonText
|
||||
padding: 14
|
||||
implicitHeight: 36
|
||||
implicitWidth: buttonTextWidget.implicitWidth + padding * 2
|
||||
buttonRadius: Appearance?.rounding.full ?? 9999
|
||||
|
||||
property color colEnabled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colDisabled: Appearance?.m3colors.m3outline ?? "#8D8C96"
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
property alias colText: buttonTextWidget.color
|
||||
|
||||
contentItem: StyledText {
|
||||
id: buttonTextWidget
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: root.padding
|
||||
anchors.rightMargin: root.padding
|
||||
text: buttonText
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
font.pixelSize: Appearance?.font.pixelSize.small ?? 12
|
||||
color: root.enabled ? root.colEnabled : root.colDisabled
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
RippleButton {
|
||||
id: root
|
||||
property bool active: false
|
||||
|
||||
horizontalPadding: Appearance.rounding.large
|
||||
verticalPadding: 12
|
||||
|
||||
clip: true
|
||||
pointingHandCursor: !active
|
||||
implicitWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: contentItem.implicitHeight + verticalPadding * 2
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
colBackground: ColorUtils.transparentize(Appearance.colors.colLayer3)
|
||||
colBackgroundHover: active ? colBackground : Appearance.colors.colLayer3Hover
|
||||
colRipple: Appearance.colors.colLayer3Active
|
||||
buttonRadius: 0
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
// From https://github.com/caelestia-dots/shell with modifications.
|
||||
// License: GPLv3
|
||||
|
||||
StyledImage {
|
||||
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,76 @@
|
|||
import qs.modules.common
|
||||
import qs.services
|
||||
import QtQuick
|
||||
|
||||
/**
|
||||
* A convenience MouseArea for handling drag events.
|
||||
*/
|
||||
MouseArea {
|
||||
id: root
|
||||
hoverEnabled: true
|
||||
acceptedButtons: Qt.LeftButton
|
||||
|
||||
property bool interactive: true
|
||||
property bool automaticallyReset: true
|
||||
readonly property real dragDiffX: _dragDiffX
|
||||
readonly property real dragDiffY: _dragDiffY
|
||||
property real startX: 0
|
||||
property real startY: 0
|
||||
property real regionTopLeftX: Math.min(startX, startX + _dragDiffX)
|
||||
property real regionTopLeftY: Math.min(startY, startY + _dragDiffY)
|
||||
property real regionWidth: Math.abs(_dragDiffX)
|
||||
property real regionHeight: Math.abs(_dragDiffY)
|
||||
|
||||
signal dragPressed(diffX: real, diffY: real)
|
||||
signal dragReleased(diffX: real, diffY: real)
|
||||
|
||||
property bool dragging: false
|
||||
property real _dragDiffX: 0
|
||||
property real _dragDiffY: 0
|
||||
|
||||
function resetDrag() {
|
||||
_dragDiffX = 0
|
||||
_dragDiffY = 0
|
||||
}
|
||||
|
||||
onPressed: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
mouse.accepted = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (mouse.button === Qt.LeftButton) {
|
||||
startX = mouse.x
|
||||
startY = mouse.y
|
||||
}
|
||||
}
|
||||
onReleased: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
return;
|
||||
}
|
||||
dragging = false
|
||||
root.dragReleased(_dragDiffX, _dragDiffY);
|
||||
if (root.automaticallyReset) {
|
||||
root.resetDrag();
|
||||
}
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
return;
|
||||
}
|
||||
if (mouse.buttons & Qt.LeftButton) {
|
||||
root._dragDiffX = mouse.x - startX
|
||||
root._dragDiffY = mouse.y - startY
|
||||
const dist = Math.sqrt(root._dragDiffX * root._dragDiffX + root._dragDiffY * root._dragDiffY);
|
||||
root.dragPressed(_dragDiffX, _dragDiffY);
|
||||
root.dragging = true;
|
||||
}
|
||||
}
|
||||
onCanceled: (mouse) => {
|
||||
if (!root.interactive) {
|
||||
return;
|
||||
}
|
||||
released(mouse);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
SequentialAnimation {
|
||||
id: root
|
||||
|
||||
required property Item target
|
||||
property real distance: 30
|
||||
|
||||
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: -root.distance; duration: 50 }
|
||||
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: root.distance; duration: 50 }
|
||||
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: -root.distance / 2; duration: 40 }
|
||||
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: root.distance / 2; duration: 40 }
|
||||
NumberAnimation { target: root.target; property: "Layout.leftMargin"; to: 0; duration: 30 }
|
||||
}
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import QtQuick
|
||||
|
||||
import qs.modules.common
|
||||
|
||||
Loader {
|
||||
id: root
|
||||
property bool shown: true
|
||||
property alias fade: opacityBehavior.enabled
|
||||
property alias animation: opacityBehavior.animation
|
||||
opacity: shown ? 1 : 0
|
||||
visible: opacity > 0
|
||||
active: opacity > 0
|
||||
|
||||
Behavior on opacity {
|
||||
id: opacityBehavior
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,48 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import qs.modules.common.functions
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import Quickshell.Io
|
||||
import Quickshell.Widgets
|
||||
|
||||
IconImage {
|
||||
id: root
|
||||
property string url
|
||||
property string displayText
|
||||
|
||||
property real size: 32
|
||||
property string downloadUserAgent: Config.options?.networking.userAgent ?? ""
|
||||
property string faviconDownloadPath: Directories.favicons
|
||||
property string domainName: url.includes("vertexaisearch") ? displayText : StringUtils.getDomain(url)
|
||||
property string faviconUrl: `https://www.google.com/s2/favicons?domain=${domainName}&sz=32`
|
||||
property string fileName: `${domainName}.ico`
|
||||
property string faviconFilePath: `${faviconDownloadPath}/${fileName}`
|
||||
property string urlToLoad
|
||||
|
||||
Process {
|
||||
id: faviconDownloadProcess
|
||||
running: false
|
||||
command: ["bash", "-c", `[ -f ${faviconFilePath} ] || curl -s '${root.faviconUrl}' -o '${faviconFilePath}' -L -H 'User-Agent: ${downloadUserAgent}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.urlToLoad = root.faviconFilePath
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
faviconDownloadProcess.running = true
|
||||
}
|
||||
|
||||
source: Qt.resolvedUrl(root.urlToLoad)
|
||||
implicitSize: root.size
|
||||
|
||||
layer.enabled: true
|
||||
layer.effect: OpacityMask {
|
||||
maskSource: Rectangle {
|
||||
width: root.implicitSize
|
||||
height: root.implicitSize
|
||||
radius: Appearance.rounding.full
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,64 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
/**
|
||||
* Material 3 FAB.
|
||||
*/
|
||||
RippleButton {
|
||||
id: root
|
||||
property string iconText: "add"
|
||||
property bool expanded: false
|
||||
property real baseSize: 56
|
||||
property real elementSpacing: 5
|
||||
implicitWidth: expanded ? (Math.max(contentRowLayout.implicitWidth + 10 * 2, baseSize)) : baseSize
|
||||
implicitHeight: baseSize
|
||||
buttonRadius: baseSize / 14 * 4
|
||||
colBackground: Appearance.colors.colPrimaryContainer
|
||||
colBackgroundHover: Appearance.colors.colPrimaryContainerHover
|
||||
colRipple: Appearance.colors.colPrimaryContainerActive
|
||||
property color colOnBackground: Appearance.colors.colOnPrimaryContainer
|
||||
contentItem: Row {
|
||||
id: contentRowLayout
|
||||
property real horizontalMargins: (root.baseSize - icon.width) / 2
|
||||
anchors {
|
||||
verticalCenter: parent?.verticalCenter
|
||||
left: parent?.left
|
||||
leftMargin: contentRowLayout.horizontalMargins
|
||||
}
|
||||
spacing: 0
|
||||
|
||||
MaterialSymbol {
|
||||
id: icon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 26
|
||||
color: root.colOnBackground
|
||||
text: root.iconText
|
||||
}
|
||||
Loader {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
visible: root.buttonText?.length > 0
|
||||
active: true
|
||||
sourceComponent: Revealer {
|
||||
visible: root.expanded || implicitWidth > 0
|
||||
reveal: root.expanded
|
||||
implicitWidth: reveal ? (buttonText.implicitWidth + root.elementSpacing + contentRowLayout.horizontalMargins) : 0
|
||||
StyledText {
|
||||
id: buttonText
|
||||
anchors {
|
||||
left: parent.left
|
||||
leftMargin: root.elementSpacing
|
||||
verticalCenter: parent.verticalCenter
|
||||
}
|
||||
text: root.buttonText
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
font.pixelSize: 14
|
||||
font.weight: 450
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
import QtQuick
|
||||
|
||||
/**
|
||||
* This is just to make sure `RippleButton`s can be used in a Flow layout.
|
||||
*/
|
||||
Flow {
|
||||
property int clickIndex: -1
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
import QtQuick
|
||||
|
||||
MouseArea { // Right side | scroll to change volume
|
||||
id: root
|
||||
|
||||
signal scrollUp(delta: int)
|
||||
signal scrollDown(delta: int)
|
||||
signal movedAway()
|
||||
|
||||
property bool hovered: false
|
||||
property real lastScrollX: 0
|
||||
property real lastScrollY: 0
|
||||
property bool trackingScroll: false
|
||||
property real moveThreshold: 20
|
||||
|
||||
acceptedButtons: Qt.LeftButton
|
||||
hoverEnabled: true
|
||||
|
||||
onEntered: {
|
||||
root.hovered = true;
|
||||
}
|
||||
|
||||
onExited: {
|
||||
root.hovered = false;
|
||||
root.trackingScroll = false;
|
||||
}
|
||||
|
||||
onWheel: event => {
|
||||
if (event.angleDelta.y < 0)
|
||||
root.scrollDown(event.angleDelta.y);
|
||||
else if (event.angleDelta.y > 0)
|
||||
root.scrollUp(event.angleDelta.y);
|
||||
// Store the mouse position and start tracking
|
||||
root.lastScrollX = event.x;
|
||||
root.lastScrollY = event.y;
|
||||
root.trackingScroll = true;
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
if (root.trackingScroll) {
|
||||
const dx = mouse.x - root.lastScrollX;
|
||||
const dy = mouse.y - root.lastScrollY;
|
||||
if (Math.sqrt(dx * dx + dy * dy) > root.moveThreshold) {
|
||||
root.movedAway();
|
||||
root.trackingScroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onContainsMouseChanged: {
|
||||
if (!root.containsMouse && root.trackingScroll) {
|
||||
root.movedAway();
|
||||
root.trackingScroll = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
required property Component contentComponent
|
||||
|
||||
Loader {
|
||||
active: PolkitService.active
|
||||
sourceComponent: Variants {
|
||||
model: Quickshell.screens
|
||||
delegate: PanelWindow {
|
||||
id: panelWindow
|
||||
required property var modelData
|
||||
screen: modelData
|
||||
|
||||
anchors {
|
||||
top: true
|
||||
left: true
|
||||
right: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
color: "transparent"
|
||||
WlrLayershell.namespace: "quickshell:polkit"
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
|
||||
Loader {
|
||||
anchors.fill: parent
|
||||
sourceComponent: root.contentComponent
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
51
surfaces/quickshell/ii-base/modules/common/widgets/Graph.qml
Normal file
51
surfaces/quickshell/ii-base/modules/common/widgets/Graph.qml
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
|
||||
/*
|
||||
* Simple one value line graph
|
||||
*/
|
||||
Canvas {
|
||||
id: root
|
||||
|
||||
enum Alignment { Left, Right }
|
||||
|
||||
required property list<real> values
|
||||
property int points: values.length
|
||||
property color color: Appearance.colors.colPrimary
|
||||
property real fillOpacity: 0.5
|
||||
property var alignment: Graph.Alignment.Left
|
||||
|
||||
onValuesChanged: root.requestPaint()
|
||||
onPaint: {
|
||||
var ctx = getContext("2d")
|
||||
ctx.clearRect(0, 0, width, height)
|
||||
if (!root.values || root.values.length < 2)
|
||||
return
|
||||
|
||||
var n = root.points
|
||||
var dx = width / (n - 1)
|
||||
ctx.strokeStyle = root.color
|
||||
ctx.fillStyle = ColorUtils.transparentize(root.color, 1 - root.fillOpacity)
|
||||
ctx.lineWidth = 2
|
||||
ctx.beginPath()
|
||||
for (var i = 0; i < n; ++i) {
|
||||
var valueIndex = (root.alignment === Graph.Alignment.Right) ? root.values.length - n + i : i
|
||||
if (valueIndex < 0 || valueIndex >= root.values.length) {
|
||||
continue; // No data for this point
|
||||
}
|
||||
var x = i * dx
|
||||
var norm = root.values[valueIndex] // already in 0-1 range
|
||||
var y = height - norm * height
|
||||
if (valueIndex === 0) {
|
||||
ctx.moveTo(x, height)
|
||||
ctx.lineTo(x, y)
|
||||
} else {
|
||||
ctx.lineTo(x, y)
|
||||
}
|
||||
}
|
||||
ctx.stroke()
|
||||
ctx.lineTo(width, height)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,147 @@
|
|||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
/**
|
||||
* Material 3 button with expressive bounciness.
|
||||
* See https://m3.material.io/components/button-groups/overview
|
||||
*/
|
||||
Button {
|
||||
id: root
|
||||
property bool toggled
|
||||
property string buttonText
|
||||
property real buttonRadius: Appearance?.rounding?.small ?? 8
|
||||
property real buttonRadiusPressed: Appearance?.rounding?.small ?? 6
|
||||
property var downAction // When left clicking (down)
|
||||
property var releaseAction // When left clicking (release)
|
||||
property var altAction // When right clicking
|
||||
property var middleClickAction // When middle clicking
|
||||
property bool bounce: true
|
||||
property real baseWidth: contentItem.implicitWidth + horizontalPadding * 2
|
||||
property real baseHeight: contentItem.implicitHeight + verticalPadding * 2
|
||||
property bool enableImplicitWidthAnimation: true
|
||||
property bool enableImplicitHeightAnimation: true
|
||||
property real clickedWidth: baseWidth + (isAtSide ? 10 : 20)
|
||||
property real clickedHeight: baseHeight
|
||||
property var parentGroup: root.parent
|
||||
property int indexInParent: parentGroup?.children.indexOf(root) ?? -1
|
||||
property int clickIndex: parentGroup?.clickIndex ?? -1
|
||||
property bool isAtSide: indexInParent === 0 || indexInParent === (parentGroup?.childrenCount - 1)
|
||||
|
||||
Layout.fillWidth: (clickIndex - 1 <= indexInParent && indexInParent <= clickIndex + 1)
|
||||
Layout.fillHeight: (clickIndex - 1 <= indexInParent && indexInParent <= clickIndex + 1)
|
||||
implicitWidth: (root.down && bounce) ? clickedWidth : baseWidth
|
||||
implicitHeight: (root.down && bounce) ? clickedHeight : baseHeight
|
||||
|
||||
property color colBackground: ColorUtils.transparentize(colBackgroundHover, 1) || "transparent"
|
||||
property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED"
|
||||
property color colBackgroundActive: Appearance?.colors.colLayer1Active ?? "#D6CEE2"
|
||||
property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F"
|
||||
property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C"
|
||||
property color colBackgroundToggledActive: Appearance?.colors.colPrimaryActive ?? "#D6CEE2"
|
||||
|
||||
property real radius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property real leftRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property real rightRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius
|
||||
property color color: root.enabled ? (root.toggled ?
|
||||
(root.down ? colBackgroundToggledActive :
|
||||
root.hovered ? colBackgroundToggledHover :
|
||||
colBackgroundToggled) :
|
||||
(root.down ? colBackgroundActive :
|
||||
root.hovered ? colBackgroundHover :
|
||||
colBackground)) : colBackground
|
||||
|
||||
onDownChanged: {
|
||||
if (root.down) {
|
||||
if (root.parent.clickIndex !== undefined) {
|
||||
root.parent.clickIndex = parent.children.indexOf(root)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Behavior on implicitWidth {
|
||||
enabled: root.enableImplicitWidthAnimation
|
||||
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on implicitHeight {
|
||||
enabled: root.enableImplicitHeightAnimation
|
||||
animation: Appearance.animation.clickBounce.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
Behavior on leftRadius {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on rightRadius {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
property alias mouseArea: buttonMouseArea
|
||||
MouseArea {
|
||||
id: buttonMouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
|
||||
onPressed: (event) => {
|
||||
if(event.button === Qt.RightButton) {
|
||||
if (root.altAction) root.altAction();
|
||||
return;
|
||||
}
|
||||
if(event.button === Qt.MiddleButton) {
|
||||
if (root.middleClickAction) root.middleClickAction();
|
||||
return;
|
||||
}
|
||||
root.down = true
|
||||
if (root.downAction) root.downAction();
|
||||
}
|
||||
onReleased: (event) => {
|
||||
root.down = false
|
||||
if (event.button != Qt.LeftButton) return;
|
||||
if (root.releaseAction) root.releaseAction();
|
||||
}
|
||||
onClicked: (event) => {
|
||||
if (event.button != Qt.LeftButton) return;
|
||||
root.click()
|
||||
}
|
||||
onCanceled: (event) => {
|
||||
root.down = false
|
||||
}
|
||||
|
||||
onPressAndHold: () => {
|
||||
// Guarded like releaseAction above. altAction is null for every
|
||||
// toggle without a menu (AndroidQuickToggleButton sets it from
|
||||
// hasMenu), so an unguarded call threw
|
||||
// "Property 'altAction' ... is not a function" on any long-press
|
||||
// of one. It only started showing up when the toggles that had
|
||||
// been silently filtered out of the panel began rendering.
|
||||
if (root.altAction) root.altAction();
|
||||
root.down = false;
|
||||
root.clicked = false;
|
||||
};
|
||||
}
|
||||
|
||||
property bool tabbedTo: root.focus && (focusReason === Qt.TabFocusReason || focusReason === Qt.BacktabFocusReason)
|
||||
background: Rectangle {
|
||||
id: buttonBackground
|
||||
topLeftRadius: root.leftRadius
|
||||
topRightRadius: root.rightRadius
|
||||
bottomLeftRadius: root.leftRadius
|
||||
bottomRightRadius: root.rightRadius
|
||||
implicitHeight: 50
|
||||
|
||||
color: root.color
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
}
|
||||
|
||||
border.width: root.tabbedTo ? 2 : 0
|
||||
border.color: Appearance.colors.colSecondary
|
||||
}
|
||||
|
||||
contentItem: StyledText {
|
||||
text: root.buttonText
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
|
||||
ToolbarButton {
|
||||
id: iconBtn
|
||||
required property string iconText
|
||||
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
|
||||
|
||||
contentItem: Row {
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
MaterialSymbol {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 22
|
||||
text: iconBtn.iconText
|
||||
color: iconBtn.colText
|
||||
}
|
||||
StyledText {
|
||||
visible: iconBtn.iconText.length > 0 && iconBtn.text.length > 0
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
color: iconBtn.colText
|
||||
text: iconBtn.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import qs.modules.common
|
||||
|
||||
ToolbarButton {
|
||||
id: iconBtn
|
||||
implicitWidth: height
|
||||
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
property color colText: toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 22
|
||||
text: iconBtn.text
|
||||
color: iconBtn.colText
|
||||
animateChange: true
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import qs.modules.common
|
||||
import QtQuick
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
property string key
|
||||
|
||||
property real horizontalPadding: 6
|
||||
property real verticalPadding: 1
|
||||
property real borderWidth: 1
|
||||
property real extraBottomBorderWidth: 2
|
||||
property color borderColor: Appearance.colors.colOnLayer0
|
||||
property real borderRadius: 5
|
||||
property real pixelSize: Appearance.font.pixelSize.smaller
|
||||
property color keyColor: Appearance.m3colors.m3surfaceContainerLow
|
||||
implicitWidth: keyFace.implicitWidth + borderWidth * 2
|
||||
implicitHeight: keyFace.implicitHeight + borderWidth * 2 + extraBottomBorderWidth
|
||||
radius: borderRadius
|
||||
color: borderColor
|
||||
|
||||
Rectangle {
|
||||
id: keyFace
|
||||
anchors {
|
||||
fill: parent
|
||||
topMargin: borderWidth
|
||||
leftMargin: borderWidth
|
||||
rightMargin: borderWidth
|
||||
bottomMargin: extraBottomBorderWidth + borderWidth
|
||||
}
|
||||
implicitWidth: keyText.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: keyText.implicitHeight + verticalPadding * 2
|
||||
color: keyColor
|
||||
radius: borderRadius - borderWidth
|
||||
|
||||
StyledText {
|
||||
id: keyText
|
||||
anchors.centerIn: parent
|
||||
font.family: Appearance.font.family.monospace
|
||||
font.pixelSize: root.pixelSize
|
||||
text: key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,122 @@
|
|||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
RippleButton {
|
||||
id: lightDarkButtonRoot
|
||||
required property bool dark
|
||||
property color previewBg: dark ? ColorUtils.colorWithHueOf("#3f3838", Appearance.m3colors.m3primary) :
|
||||
ColorUtils.colorWithHueOf("#F7F9FF", Appearance.m3colors.m3primary)
|
||||
property color previewFg: dark ? Qt.lighter(previewBg, 2.2) : ColorUtils.mix(previewBg, "#292929", 0.85)
|
||||
padding: 5
|
||||
Layout.fillWidth: true
|
||||
colBackground: Appearance.colors.colLayer2
|
||||
toggled: Appearance.m3colors.darkmode === dark
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["bash", "-c", `${Directories.wallpaperSwitchScriptPath} --mode ${dark ? "dark" : "light"} --noswitch`])
|
||||
}
|
||||
contentItem: Item {
|
||||
anchors.centerIn: parent
|
||||
implicitWidth: buttonContentLayout.implicitWidth
|
||||
implicitHeight: buttonContentLayout.implicitHeight
|
||||
ColumnLayout {
|
||||
id: buttonContentLayout
|
||||
anchors.centerIn: parent
|
||||
Rectangle {
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
implicitWidth: 250
|
||||
implicitHeight: skeletonColumnLayout.implicitHeight + 10 * 2
|
||||
radius: lightDarkButtonRoot.buttonRadius - lightDarkButtonRoot.padding
|
||||
color: lightDarkButtonRoot.previewBg
|
||||
border {
|
||||
width: 1
|
||||
color: Appearance.m3colors.m3outlineVariant
|
||||
}
|
||||
|
||||
// Some skeleton items
|
||||
ColumnLayout {
|
||||
id: skeletonColumnLayout
|
||||
anchors.fill: parent
|
||||
anchors.margins: 10
|
||||
spacing: 10
|
||||
RowLayout {
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.full
|
||||
color: lightDarkButtonRoot.previewFg
|
||||
implicitWidth: 50
|
||||
implicitHeight: 50
|
||||
}
|
||||
ColumnLayout {
|
||||
spacing: 4
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.unsharpenmore
|
||||
color: lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 22
|
||||
}
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.unsharpenmore
|
||||
color: lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
Layout.rightMargin: 45
|
||||
implicitHeight: 18
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledProgressBar {
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 5
|
||||
Layout.fillWidth: true
|
||||
value: 0.7
|
||||
wavy: true
|
||||
animateWave: lightDarkButtonRoot.toggled
|
||||
highlightColor: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg
|
||||
trackColor: ColorUtils.mix(lightDarkButtonRoot.previewBg, lightDarkButtonRoot.previewFg, 0.5)
|
||||
}
|
||||
RowLayout {
|
||||
spacing: 2
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.full
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3primary : lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
MaterialSymbol {
|
||||
visible: lightDarkButtonRoot.toggled
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "check"
|
||||
iconSize: 20
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : lightDarkButtonRoot.previewBg
|
||||
}
|
||||
}
|
||||
Rectangle {
|
||||
radius: Appearance.rounding.unsharpenmore
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
}
|
||||
Rectangle {
|
||||
topLeftRadius: Appearance.rounding.unsharpenmore
|
||||
bottomLeftRadius: Appearance.rounding.unsharpenmore
|
||||
topRightRadius: Appearance.rounding.full
|
||||
bottomRightRadius: Appearance.rounding.full
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3secondaryContainer : lightDarkButtonRoot.previewFg
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: 30
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
StyledText {
|
||||
Layout.fillWidth: true
|
||||
text: dark ? Translation.tr("Dark") : Translation.tr("Light")
|
||||
color: lightDarkButtonRoot.toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
import QtQuick
|
||||
import QtQuick.Effects
|
||||
|
||||
// Note: You still have to set sizes yourself
|
||||
MultiEffect {
|
||||
maskEnabled: true
|
||||
maskThresholdMin: 0.5
|
||||
maskSpreadAtMin: 1
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets.shapes
|
||||
import "shapes/geometry/offset.js" as Offset
|
||||
import "shapes/shapes/corner-rounding.js" as CornerRounding
|
||||
import "shapes/shapes/rounded-polygon.js" as RoundedPolygon
|
||||
import "shapes/material-shapes.js" as MaterialShapes
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int sides: 12
|
||||
property int implicitSize: 100
|
||||
property alias color: shapeCanvas.color
|
||||
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
|
||||
property var cornerRounding: new CornerRounding.CornerRounding((sides < 17 ? 1.5 : 1.1) / Math.max(sides, 1))
|
||||
|
||||
ShapeCanvas {
|
||||
id: shapeCanvas
|
||||
anchors.fill: parent
|
||||
roundedPolygon: switch(sides) {
|
||||
case 0: return MaterialShapes.getCircle();
|
||||
case 1: return MaterialShapes.getCircle();
|
||||
case 4: return MaterialShapes.getCookie4Sided();
|
||||
case 6: return MaterialShapes.getCookie6Sided();
|
||||
case 7: return MaterialShapes.getCookie7Sided();
|
||||
case 9: return MaterialShapes.getCookie9Sided();
|
||||
case 12: return MaterialShapes.getCookie12Sided();
|
||||
default: return RoundedPolygon.RoundedPolygon.star(sides, 1, 0.8, root.cornerRounding)
|
||||
.transformed((x, y) => MaterialShapes.rotate30.map(new Offset.Offset(x, y)))
|
||||
.normalized();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,89 @@
|
|||
pragma ComponentBehavior: Bound
|
||||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
|
||||
property bool loading: true
|
||||
property double pullProgress: 0
|
||||
|
||||
// Size, color
|
||||
property double implicitSize: 48
|
||||
implicitWidth: implicitSize
|
||||
implicitHeight: implicitSize
|
||||
radius: Math.min(width, height) / 2
|
||||
color: Appearance.colors.colPrimaryContainer
|
||||
property double baseShapeSize: root.implicitSize * 0.7
|
||||
property double leapZoomSize: root.baseShapeSize * 1.2
|
||||
property double leapZoomProgress: 0
|
||||
|
||||
// Shape
|
||||
property list<var> shapes: [
|
||||
MaterialShape.Shape.SoftBurst,
|
||||
MaterialShape.Shape.Cookie9Sided,
|
||||
MaterialShape.Shape.Pentagon,
|
||||
MaterialShape.Shape.Pill,
|
||||
MaterialShape.Shape.Sunny,
|
||||
MaterialShape.Shape.Cookie4Sided,
|
||||
MaterialShape.Shape.Oval,
|
||||
]
|
||||
property int shapeIndex: 0
|
||||
property double pullRotation: root.loading ? 0 : -(root.pullProgress * 360)
|
||||
property double continuousRotation: 0
|
||||
property double leapRotation: 0
|
||||
rotation: pullRotation + continuousRotation + leapRotation
|
||||
|
||||
RotationAnimation on continuousRotation {
|
||||
running: root.loading
|
||||
duration: 12000
|
||||
easing.type: Easing.Linear
|
||||
loops: Animation.Infinite
|
||||
from: 0
|
||||
to: 360
|
||||
}
|
||||
Timer {
|
||||
interval: 800
|
||||
running: root.loading
|
||||
repeat: true
|
||||
onTriggered: leapAnimation.start()
|
||||
}
|
||||
ParallelAnimation {
|
||||
id: leapAnimation
|
||||
PropertyAction { target: root; property: "shapeIndex"; value: (root.shapeIndex + 1) % root.shapes.length }
|
||||
RotationAnimation {
|
||||
target: root
|
||||
direction: RotationAnimation.Shortest
|
||||
property: "leapRotation"
|
||||
to: (root.leapRotation + 90) % 360
|
||||
duration: 350
|
||||
easing.type: Easing.InOutQuad
|
||||
}
|
||||
NumberAnimation {
|
||||
target: root
|
||||
property: "leapZoomProgress"
|
||||
from: 0
|
||||
to: 1
|
||||
duration: 750
|
||||
easing.type: Easing.BezierSpline
|
||||
easing.bezierCurve: Appearance.animationCurves.standard
|
||||
}
|
||||
}
|
||||
|
||||
MaterialShape {
|
||||
id: shape
|
||||
anchors.centerIn: parent
|
||||
shape: root.shapes[root.shapeIndex]
|
||||
implicitSize: {
|
||||
const leapZoomDiff = root.leapZoomSize - root.baseShapeSize
|
||||
const progressFirstHalf = Math.min(root.leapZoomProgress, 0.5) * 2;
|
||||
const progressSecondHalf = Math.max(root.leapZoomProgress - 0.5, 0) * 2;
|
||||
return root.baseShapeSize + leapZoomDiff * progressFirstHalf - leapZoomDiff * progressSecondHalf;
|
||||
}
|
||||
color: Appearance.colors.colOnPrimaryContainer
|
||||
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
import qs.modules.common.widgets.shapes
|
||||
import "shapes/material-shapes.js" as MaterialShapes
|
||||
|
||||
ShapeCanvas {
|
||||
id: root
|
||||
enum Shape {
|
||||
Circle,
|
||||
Square,
|
||||
Slanted,
|
||||
Arch,
|
||||
Fan,
|
||||
Arrow,
|
||||
SemiCircle,
|
||||
Oval,
|
||||
Pill,
|
||||
Triangle,
|
||||
Diamond,
|
||||
ClamShell,
|
||||
Pentagon,
|
||||
Gem,
|
||||
Sunny,
|
||||
VerySunny,
|
||||
Cookie4Sided,
|
||||
Cookie6Sided,
|
||||
Cookie7Sided,
|
||||
Cookie9Sided,
|
||||
Cookie12Sided,
|
||||
Ghostish,
|
||||
Clover4Leaf,
|
||||
Clover8Leaf,
|
||||
Burst,
|
||||
SoftBurst,
|
||||
Boom,
|
||||
SoftBoom,
|
||||
Flower,
|
||||
Puffy,
|
||||
PuffyDiamond,
|
||||
PixelCircle,
|
||||
PixelTriangle,
|
||||
Bun,
|
||||
Heart
|
||||
}
|
||||
required property var shape
|
||||
property double implicitSize
|
||||
implicitHeight: implicitSize
|
||||
implicitWidth: implicitSize
|
||||
polygonIsNormalized: true
|
||||
roundedPolygon: {
|
||||
switch (root.shape) {
|
||||
case MaterialShape.Shape.Circle: return MaterialShapes.getCircle();
|
||||
case MaterialShape.Shape.Square: return MaterialShapes.getSquare();
|
||||
case MaterialShape.Shape.Slanted: return MaterialShapes.getSlanted();
|
||||
case MaterialShape.Shape.Arch: return MaterialShapes.getArch();
|
||||
case MaterialShape.Shape.Fan: return MaterialShapes.getFan();
|
||||
case MaterialShape.Shape.Arrow: return MaterialShapes.getArrow();
|
||||
case MaterialShape.Shape.SemiCircle: return MaterialShapes.getSemiCircle();
|
||||
case MaterialShape.Shape.Oval: return MaterialShapes.getOval();
|
||||
case MaterialShape.Shape.Pill: return MaterialShapes.getPill();
|
||||
case MaterialShape.Shape.Triangle: return MaterialShapes.getTriangle();
|
||||
case MaterialShape.Shape.Diamond: return MaterialShapes.getDiamond();
|
||||
case MaterialShape.Shape.ClamShell: return MaterialShapes.getClamShell();
|
||||
case MaterialShape.Shape.Pentagon: return MaterialShapes.getPentagon();
|
||||
case MaterialShape.Shape.Gem: return MaterialShapes.getGem();
|
||||
case MaterialShape.Shape.Sunny: return MaterialShapes.getSunny();
|
||||
case MaterialShape.Shape.VerySunny: return MaterialShapes.getVerySunny();
|
||||
case MaterialShape.Shape.Cookie4Sided: return MaterialShapes.getCookie4Sided();
|
||||
case MaterialShape.Shape.Cookie6Sided: return MaterialShapes.getCookie6Sided();
|
||||
case MaterialShape.Shape.Cookie7Sided: return MaterialShapes.getCookie7Sided();
|
||||
case MaterialShape.Shape.Cookie9Sided: return MaterialShapes.getCookie9Sided();
|
||||
case MaterialShape.Shape.Cookie12Sided: return MaterialShapes.getCookie12Sided();
|
||||
case MaterialShape.Shape.Ghostish: return MaterialShapes.getGhostish();
|
||||
case MaterialShape.Shape.Clover4Leaf: return MaterialShapes.getClover4Leaf();
|
||||
case MaterialShape.Shape.Clover8Leaf: return MaterialShapes.getClover8Leaf();
|
||||
case MaterialShape.Shape.Burst: return MaterialShapes.getBurst();
|
||||
case MaterialShape.Shape.SoftBurst: return MaterialShapes.getSoftBurst();
|
||||
case MaterialShape.Shape.Boom: return MaterialShapes.getBoom();
|
||||
case MaterialShape.Shape.SoftBoom: return MaterialShapes.getSoftBoom();
|
||||
case MaterialShape.Shape.Flower: return MaterialShapes.getFlower();
|
||||
case MaterialShape.Shape.Puffy: return MaterialShapes.getPuffy();
|
||||
case MaterialShape.Shape.PuffyDiamond: return MaterialShapes.getPuffyDiamond();
|
||||
case MaterialShape.Shape.PixelCircle: return MaterialShapes.getPixelCircle();
|
||||
case MaterialShape.Shape.PixelTriangle: return MaterialShapes.getPixelTriangle();
|
||||
case MaterialShape.Shape.Bun: return MaterialShapes.getBun();
|
||||
case MaterialShape.Shape.Heart: return MaterialShapes.getHeart();
|
||||
default: return MaterialShapes.getCircle();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,23 @@
|
|||
import QtQuick
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
|
||||
MaterialShape {
|
||||
id: root
|
||||
property alias text: symbol.text
|
||||
property alias iconSize: symbol.iconSize
|
||||
property alias font: symbol.font
|
||||
property alias colSymbol: symbol.color
|
||||
property real padding: 6
|
||||
|
||||
color: Appearance.colors.colSecondaryContainer
|
||||
colSymbol: Appearance.colors.colOnSecondaryContainer
|
||||
shape: MaterialShape.Shape.Clover4Leaf
|
||||
implicitSize: Math.max(symbol.implicitWidth, symbol.implicitHeight) + padding * 2
|
||||
|
||||
MaterialSymbol {
|
||||
id: symbol
|
||||
anchors.centerIn: parent
|
||||
color: root.colSymbol
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue