forked from Shinonome/dots-hyprland
Merge branch 'end-4:main' into patch-1
This commit is contained in:
@@ -0,0 +1,167 @@
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
name: Close issues when the "ticked without reading" checkbox is checked
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
jobs:
|
||||
detect-and-close:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Detect checked "ticked without reading" checkbox, comment, close and lock
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
OWNER: ${{ github.repository_owner }}
|
||||
REPO: ${{ github.event.repository.name }}
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_BODY: ${{ toJson(github.event.issue.body) }}
|
||||
ISSUE_USER: ${{ github.event.issue.user.login }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
|
||||
# Normalize the JSON-encoded body into plain text
|
||||
BODY=$(printf '%s' "$ISSUE_BODY" | sed -E 's/^"(.*)"$/\1/' | sed 's/\\"/"/g' | sed 's/\\n/\n/g')
|
||||
|
||||
echo "Checking issue #${ISSUE_NUMBER} for the target checked checkbox..."
|
||||
# Use -- to stop option parsing so the leading - in the pattern isn't treated as an option
|
||||
if printf '%s' "$BODY" | grep -Fiq -- "- [x] I've ticked the checkboxes without reading their contents"; then
|
||||
echo "Target checkbox is checked. Proceeding to comment, close and lock the issue."
|
||||
|
||||
# --- Get issue node id via GraphQL ---
|
||||
QUERY='query($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { issue(number: $number) { id } } }'
|
||||
GET_ID_PAYLOAD=$(jq -n --arg q "$QUERY" --arg owner "$OWNER" --arg name "$REPO" --argjson number "$ISSUE_NUMBER" '{query:$q, variables:{owner:$owner, name:$name, number:$number}}')
|
||||
|
||||
echo "GraphQL: fetching issue node id..."
|
||||
RES=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$GET_ID_PAYLOAD" https://api.github.com/graphql)
|
||||
echo "GraphQL response (get id):"
|
||||
printf '%s\n' "$RES"
|
||||
|
||||
ISSUE_ID=$(printf '%s' "$RES" | jq -r '.data.repository.issue.id // empty')
|
||||
|
||||
if [ -z "$ISSUE_ID" ]; then
|
||||
echo "Failed to get issue id from GraphQL response. Aborting."
|
||||
exit 1
|
||||
fi
|
||||
echo "Issue node id: $ISSUE_ID"
|
||||
|
||||
# --- Post a comment to the issue ---
|
||||
COMMENT_BODY="Hi @${ISSUE_USER} — I noticed you checked \"I've ticked the checkboxes without reading their contents\" in the issue template. To help others assist you effectively, please read the template and provide the requested diagnostic information (Step 2 & Step 3). I will close this issue now. If you create a new issue with the required information, we can re-evaluate. Thank you!"
|
||||
MUT_ADD_COMMENT='mutation($id: ID!, $body: String!) { addComment(input: {subjectId: $id, body: $body}) { clientMutationId } }'
|
||||
ADD_COMMENT_PAYLOAD=$(jq -n --arg q "$MUT_ADD_COMMENT" --arg id "$ISSUE_ID" --arg body "$COMMENT_BODY" '{query:$q, variables:{id:$id, body:$body}}')
|
||||
|
||||
echo "GraphQL: adding comment..."
|
||||
RES_COMMENT=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$ADD_COMMENT_PAYLOAD" https://api.github.com/graphql)
|
||||
echo "GraphQL response (add comment):"
|
||||
printf '%s\n' "$RES_COMMENT"
|
||||
|
||||
ERR_COMMENT=$(printf '%s' "$RES_COMMENT" | jq -r '.errors[]?.message // empty')
|
||||
if [ -n "$ERR_COMMENT" ]; then
|
||||
echo "addComment error: $ERR_COMMENT"
|
||||
exit 1
|
||||
fi
|
||||
echo "Comment posted."
|
||||
|
||||
# --- Attempt to close via GraphQL updateIssue ---
|
||||
MUT_UPDATE_ISSUE='mutation($id: ID!) { updateIssue(input: {id: $id, state: CLOSED, stateReason: NOT_PLANNED}) { issue { number, state, stateReason } } }'
|
||||
UPDATE_PAYLOAD=$(jq -n --arg q "$MUT_UPDATE_ISSUE" --arg id "$ISSUE_ID" '{query:$q, variables:{id:$id}}')
|
||||
|
||||
echo "GraphQL: updating issue (close with NOT_PLANNED)..."
|
||||
RES_UPDATE=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$UPDATE_PAYLOAD" https://api.github.com/graphql)
|
||||
echo "GraphQL response (update issue):"
|
||||
printf '%s\n' "$RES_UPDATE"
|
||||
|
||||
ERR_UPDATE=$(printf '%s' "$RES_UPDATE" | jq -r '.errors[]?.message // empty')
|
||||
UPDATED_STATE=$(printf '%s' "$RES_UPDATE" | jq -r '.data.updateIssue.issue.state // empty')
|
||||
UPDATED_REASON=$(printf '%s' "$RES_UPDATE" | jq -r '.data.updateIssue.issue.stateReason // empty')
|
||||
|
||||
CLOSED_OK=false
|
||||
|
||||
if [ -n "$ERR_UPDATE" ]; then
|
||||
echo "GraphQL updateIssue returned errors: $ERR_UPDATE"
|
||||
fi
|
||||
|
||||
if [ "$UPDATED_STATE" = "CLOSED" ]; then
|
||||
echo "Issue closed via GraphQL: state=$UPDATED_STATE, stateReason=$UPDATED_REASON"
|
||||
CLOSED_OK=true
|
||||
else
|
||||
echo "GraphQL update did not confirm the issue is closed. Falling back to REST API PATCH to ensure the issue is closed."
|
||||
|
||||
# REST fallback to close the issue with state_reason "not_planned"
|
||||
REST_PAYLOAD=$(jq -n --arg state "closed" --arg sr "not_planned" '{state:$state, state_reason:$sr}')
|
||||
echo "REST: PATCH /repos/$OWNER/$REPO/issues/$ISSUE_NUMBER payload: $REST_PAYLOAD"
|
||||
RES_REST=$(curl -s -w "\n%{http_code}" -X PATCH \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "$REST_PAYLOAD" \
|
||||
"https://api.github.com/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER")
|
||||
|
||||
HTTP_STATUS=$(printf '%s' "$RES_REST" | tail -n1)
|
||||
RESP_BODY=$(printf '%s' "$RES_REST" | sed '$d')
|
||||
|
||||
echo "REST response body:"
|
||||
printf '%s\n' "$RESP_BODY"
|
||||
echo "REST HTTP status: $HTTP_STATUS"
|
||||
|
||||
if [ "$HTTP_STATUS" -ge 200 ] && [ "$HTTP_STATUS" -lt 300 ]; then
|
||||
CLOSED_STATE=$(printf '%s' "$RESP_BODY" | jq -r '.state // empty')
|
||||
CLOSED_REASON=$(printf '%s' "$RESP_BODY" | jq -r '.state_reason // empty')
|
||||
echo "Issue closed via REST: state=$CLOSED_STATE, state_reason=$CLOSED_REASON"
|
||||
if [ "$CLOSED_STATE" = "closed" ]; then
|
||||
CLOSED_OK=true
|
||||
fi
|
||||
else
|
||||
echo "REST fallback failed to close the issue. See REST response above."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Attempt to lock the conversation (GraphQL first, then REST fallback) ---
|
||||
if [ "$CLOSED_OK" = "true" ]; then
|
||||
echo "Attempting to lock the conversation via GraphQL with reason NO_REASON..."
|
||||
|
||||
MUT_LOCK='mutation($id: ID!, $reason: LockReason) { lockLockable(input:{lockableId:$id, lockReason:$reason}) { clientMutationId } }'
|
||||
LOCK_PAYLOAD=$(jq -n --arg q "$MUT_LOCK" --arg id "$ISSUE_ID" --arg reason "NO_REASON" '{query:$q, variables:{id:$id, reason:$reason}}')
|
||||
|
||||
RES_LOCK=$(curl -s -H "Authorization: Bearer $GITHUB_TOKEN" -H "Content-Type: application/json" -d "$LOCK_PAYLOAD" https://api.github.com/graphql)
|
||||
echo "GraphQL response (lock):"
|
||||
printf '%s\n' "$RES_LOCK"
|
||||
|
||||
LOCK_ERR=$(printf '%s' "$RES_LOCK" | jq -r '.errors[]?.message // empty')
|
||||
|
||||
if [ -n "$LOCK_ERR" ]; then
|
||||
echo "GraphQL lockLockable returned errors: $LOCK_ERR"
|
||||
echo "Falling back to REST API to lock the conversation (no explicit reason)."
|
||||
|
||||
# REST fallback to lock the issue (no lock_reason to indicate "no reason")
|
||||
RES_REST_LOCK=$(curl -s -w "\n%{http_code}" -X PUT \
|
||||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
"https://api.github.com/repos/$OWNER/$REPO/issues/$ISSUE_NUMBER/lock" -d '{}')
|
||||
|
||||
HTTP_STATUS_LOCK=$(printf '%s' "$RES_REST_LOCK" | tail -n1)
|
||||
RESP_BODY_LOCK=$(printf '%s' "$RES_REST_LOCK" | sed '$d')
|
||||
|
||||
echo "REST lock response body:"
|
||||
printf '%s\n' "$RESP_BODY_LOCK"
|
||||
echo "REST lock HTTP status: $HTTP_STATUS_LOCK"
|
||||
|
||||
if [ "$HTTP_STATUS_LOCK" -ge 200 ] && [ "$HTTP_STATUS_LOCK" -lt 300 ]; then
|
||||
echo "Issue conversation locked via REST (no explicit reason)."
|
||||
else
|
||||
echo "REST fallback failed to lock the conversation. See REST response above."
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
echo "Lock via GraphQL succeeded (or returned no errors)."
|
||||
fi
|
||||
else
|
||||
echo "Issue was not successfully closed; skipping lock."
|
||||
fi
|
||||
|
||||
else
|
||||
echo "Checkbox not present/checked. Nothing to do."
|
||||
fi
|
||||
@@ -60,6 +60,7 @@ bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call T
|
||||
bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback)
|
||||
bind = Super+Shift, S, global, quickshell:regionScreenshot # Screen snip
|
||||
bind = Super+Shift, S, exec, qs -c $qsConfig ipc call TEST_ALIVE || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # [hidden] Screen snip (fallback)
|
||||
bind = Super+Shift, A, global, quickshell:regionSearch # Google Lens
|
||||
# OCR
|
||||
bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
|
||||
# Color picker
|
||||
@@ -225,8 +226,8 @@ binde = Super, Equal, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in
|
||||
binde = Super, Minus, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out
|
||||
binde = Super, Equal, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
|
||||
# Zoom with keypad
|
||||
binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # Zoom out
|
||||
binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # Zoom in
|
||||
binde = Super, code:82, exec, qs -c $qsConfig ipc call zoom zoomOut # [hidden] Zoom out
|
||||
binde = Super, code:86, exec, qs -c $qsConfig ipc call zoom zoomIn # [hidden] Zoom in
|
||||
binde = Super, code:82, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh decrease 0.1 # [hidden] Zoom out
|
||||
binde = Super, code:86, exec, qs -c $qsConfig ipc call TEST_ALIVE || ~/.config/hypr/hyprland/scripts/zoom.sh increase 0.1 # [hidden] Zoom in
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
||||
<svg
|
||||
id="mdi-gentoo"
|
||||
viewBox="0 0 20 20"
|
||||
version="1.1"
|
||||
sodipodi:docname="Pictogrammers-Material-Gentoo.svg"
|
||||
width="20"
|
||||
height="20"
|
||||
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
|
||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:svg="http://www.w3.org/2000/svg">
|
||||
<defs
|
||||
id="defs1" />
|
||||
<sodipodi:namedview
|
||||
id="namedview1"
|
||||
pagecolor="#ffffff"
|
||||
bordercolor="#000000"
|
||||
borderopacity="0.25"
|
||||
inkscape:showpageshadow="2"
|
||||
inkscape:pageopacity="0.0"
|
||||
inkscape:pagecheckerboard="0"
|
||||
inkscape:deskcolor="#d1d1d1"
|
||||
inkscape:zoom="20.875"
|
||||
inkscape:cx="9.508982"
|
||||
inkscape:cy="9.9640719"
|
||||
inkscape:window-width="1327"
|
||||
inkscape:window-height="1068"
|
||||
inkscape:window-x="0"
|
||||
inkscape:window-y="0"
|
||||
inkscape:window-maximized="1"
|
||||
inkscape:current-layer="mdi-gentoo" />
|
||||
<path
|
||||
d="m 8.2792823,-5.5983568e-4 c -0.35,0 -0.71,0.0299999957 -1.05,0.0999999957 -3.62,0.66 -6.17,3.79000004 -6.38,5.86000004 -0.11,1.01 0.44,1.77 0.74,2.1 0.81,0.91 2.44,1.6 3.48,2.1699998 -1.51,1.27 -2.2,1.91 -2.88,2.63 -1.02,1.07 -1.74,2.24 -1.74,3.09 0,0.27 -0.05,1.14 0.31,1.82 0.13,0.26 0.51,1.12 1.65,1.76 0.73,0.41 1.76,0.56 2.78,0.42 3.14,-0.45 7.3499997,-3.12 10.3599997,-5.6 1.91,-1.58 3.31,-3.12 3.71,-3.85 0.33,-0.6299998 0.37,-1.7199998 0.18,-2.4099998 -0.54,-1.95 -4.91,-5.94 -8.48,-7.54000004 -0.82,-0.37 -1.7599997,-0.54999999568 -2.6799997,-0.54999999568 m 1.06,2.91000003568 c 0.25,0 0.47,0.03 0.66,0.09 1.1499997,0.3 3.0799997,1.68 2.9099997,2.94 -0.23,1.66 -1.68,2.33 -3.3499997,2.09 -0.98,-0.13 -2.93,-1.23 -2.78,-3.14 0.11,-1.49 1.52,-1.99 2.56,-1.98 m -0.02,1.74 c -0.27,0 -0.48,0.06 -0.58,0.22 -0.47,0.72 -0.24,1.22 0.18,1.55 0.15,-0.38 1.7899997,0.03 1.8299997,0.37 1.42,-1.07 -0.39,-2.13 -1.4299997,-2.14 z"
|
||||
id="path1" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -36,6 +36,7 @@ ApplicationWindow {
|
||||
|
||||
Component.onCompleted: {
|
||||
Config.readWriteDelay = 0;
|
||||
Config.blockWrites = true;
|
||||
MaterialThemeLoader.reapplyTheme();
|
||||
}
|
||||
|
||||
@@ -90,8 +91,8 @@ ApplicationWindow {
|
||||
}
|
||||
onClicked: {
|
||||
Quickshell.execDetached(["killall", ...conflictGroup.programs])
|
||||
conflictGroup.visible = false
|
||||
conflictGroup.alwaysSelected()
|
||||
conflictGroup.visible = false
|
||||
}
|
||||
}
|
||||
RippleButton {
|
||||
|
||||
@@ -13,7 +13,7 @@ import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "./cookieClock"
|
||||
import qs.modules.background.cookieClock
|
||||
|
||||
Variants {
|
||||
id: root
|
||||
|
||||
@@ -9,8 +9,8 @@ import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell.Io
|
||||
|
||||
import "./dateIndicator"
|
||||
import "./minuteMarks"
|
||||
import qs.modules.background.cookieClock.dateIndicator
|
||||
import qs.modules.background.cookieClock.minuteMarks
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import "./weather"
|
||||
import qs.modules.bar.weather
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
@@ -25,7 +25,7 @@ Item {
|
||||
visible: Config.options.bar.utilButtons.showScreenSnip
|
||||
sourceComponent: CircleUtilButton {
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
|
||||
onClicked: Hyprland.dispatch("global quickshell:regionScreenshot")
|
||||
MaterialSymbol {
|
||||
horizontalAlignment: Qt.AlignHCenter
|
||||
fill: 1
|
||||
|
||||
@@ -4,7 +4,7 @@ import qs.modules.common.widgets
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../"
|
||||
import qs.modules.bar
|
||||
|
||||
StyledPopup {
|
||||
id: root
|
||||
@@ -101,4 +101,4 @@ StyledPopup {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ Singleton {
|
||||
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(".");
|
||||
@@ -63,6 +64,7 @@ Singleton {
|
||||
id: configFileView
|
||||
path: root.filePath
|
||||
watchChanges: true
|
||||
blockWrites: root.blockWrites
|
||||
onFileChanged: fileReloadTimer.restart()
|
||||
onAdapterUpdated: fileWriteTimer.restart()
|
||||
onLoaded: root.ready = true
|
||||
@@ -302,6 +304,9 @@ Singleton {
|
||||
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 {
|
||||
@@ -349,6 +354,24 @@ Singleton {
|
||||
property real columns: 5
|
||||
}
|
||||
|
||||
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 JsonObject rect: JsonObject {
|
||||
property bool showAimLines: true
|
||||
}
|
||||
property JsonObject circle: JsonObject {
|
||||
property int strokeWidth: 6
|
||||
property int padding: 30
|
||||
}
|
||||
}
|
||||
|
||||
property JsonObject resources: JsonObject {
|
||||
property int updateInterval: 3000
|
||||
}
|
||||
@@ -368,6 +391,10 @@ Singleton {
|
||||
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 sidebar: JsonObject {
|
||||
@@ -454,10 +481,6 @@ Singleton {
|
||||
property int arbitraryRaceConditionDelay: 20 // milliseconds
|
||||
}
|
||||
|
||||
property JsonObject screenshotTool: JsonObject {
|
||||
property bool showContentRegions: true
|
||||
}
|
||||
|
||||
property JsonObject workSafety: JsonObject {
|
||||
property JsonObject enable: JsonObject {
|
||||
property bool wallpaper: true
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
import Quickshell
|
||||
import "./fuzzysort.js" as FuzzySort
|
||||
import "fuzzysort.js" as FuzzySort
|
||||
|
||||
/**
|
||||
* Wrapper for FuzzySort to play nicely with Quickshell's imports
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
pragma Singleton
|
||||
import Quickshell
|
||||
import "./levendist.js" as Levendist
|
||||
import "levendist.js" as Levendist
|
||||
|
||||
/**
|
||||
* Wrapper for levendist.js to play nicely with Quickshell's imports
|
||||
|
||||
@@ -7,6 +7,7 @@ import QtQuick.Controls
|
||||
RippleButton {
|
||||
id: root
|
||||
property string buttonIcon
|
||||
property alias iconSize: iconWidget.iconSize
|
||||
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: contentItem.implicitHeight + 8 * 2
|
||||
@@ -17,6 +18,7 @@ RippleButton {
|
||||
contentItem: RowLayout {
|
||||
spacing: 10
|
||||
OptionalMaterialSymbol {
|
||||
id: iconWidget
|
||||
icon: root.buttonIcon
|
||||
opacity: root.enabled ? 1 : 0.4
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
|
||||
@@ -22,6 +22,8 @@ Button {
|
||||
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
|
||||
@@ -61,10 +63,12 @@ Button {
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -75,7 +79,9 @@ Button {
|
||||
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
|
||||
|
||||
@@ -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: 6
|
||||
|
||||
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,21 @@
|
||||
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
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 22
|
||||
text: iconBtn.text
|
||||
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import qs.modules.common
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import "notification_utils.js" as NotificationUtils
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import "./notification_utils.js" as NotificationUtils
|
||||
import "notification_utils.js" as NotificationUtils
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
@@ -73,12 +73,13 @@ Slider {
|
||||
|
||||
component TrackDot: Rectangle {
|
||||
required property real value
|
||||
property real normalizedValue: (value - root.from) / (root.to - root.from)
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
x: root.handleMargins + (value * root.effectiveDraggingWidth) - (root.trackDotSize / 2)
|
||||
x: root.handleMargins + (normalizedValue * root.effectiveDraggingWidth) - (root.trackDotSize / 2)
|
||||
width: root.trackDotSize
|
||||
height: root.trackDotSize
|
||||
radius: Appearance.rounding.full
|
||||
color: value > root.visualPosition ? root.dotColor : root.dotColorHighlighted
|
||||
color: normalizedValue > root.visualPosition ? root.dotColor : root.dotColorHighlighted
|
||||
|
||||
Behavior on color {
|
||||
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
|
||||
|
||||
@@ -50,7 +50,7 @@ Rectangle {
|
||||
property real targetY: root.height / 2 - root.backgroundHeight / 2
|
||||
y: root.show ? targetY : (targetY - root.backgroundAnimationMovementDistance)
|
||||
implicitWidth: 350
|
||||
implicitHeight: 0
|
||||
implicitHeight: contentColumn.implicitHeight + dialogBackground.radius * 2
|
||||
Behavior on implicitHeight {
|
||||
NumberAnimation {
|
||||
id: dialogBackgroundHeightAnimation
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Widgets
|
||||
|
||||
Column {
|
||||
id: root
|
||||
|
||||
property alias text: sliderName.text
|
||||
property alias from: sliderWidget.from
|
||||
property alias to: sliderWidget.to
|
||||
property alias value: sliderWidget.value
|
||||
property alias tooltipContent: sliderWidget.tooltipContent
|
||||
property alias stopIndicatorValues: sliderWidget.stopIndicatorValues
|
||||
|
||||
signal moved()
|
||||
|
||||
spacing: -2
|
||||
ContentSubsectionLabel {
|
||||
id: sliderName
|
||||
visible: text?.length > 0
|
||||
text: ""
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
}
|
||||
StyledSlider {
|
||||
id: sliderWidget
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: 4
|
||||
rightMargin: leftMargin
|
||||
}
|
||||
configuration: StyledSlider.Configuration.S
|
||||
onMoved: root.moved()
|
||||
}
|
||||
}
|
||||
@@ -234,26 +234,26 @@ MouseArea {
|
||||
color: (Battery.isLow && !Battery.isCharging) ? Appearance.colors.colError : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
|
||||
ActionToolbarIconButton {
|
||||
IconToolbarButton {
|
||||
id: sleepButton
|
||||
onClicked: Session.suspend()
|
||||
text: "dark_mode"
|
||||
}
|
||||
|
||||
PasswordGuardedActionToolbarIconButton {
|
||||
PasswordGuardedIconToolbarButton {
|
||||
id: powerButton
|
||||
text: "power_settings_new"
|
||||
targetAction: LockContext.ActionEnum.Poweroff
|
||||
}
|
||||
|
||||
PasswordGuardedActionToolbarIconButton {
|
||||
PasswordGuardedIconToolbarButton {
|
||||
id: rebootButton
|
||||
text: "restart_alt"
|
||||
targetAction: LockContext.ActionEnum.Reboot
|
||||
}
|
||||
}
|
||||
|
||||
component PasswordGuardedActionToolbarIconButton: ActionToolbarIconButton {
|
||||
component PasswordGuardedIconToolbarButton: IconToolbarButton {
|
||||
id: guardedBtn
|
||||
required property var targetAction
|
||||
|
||||
@@ -273,24 +273,6 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
component ActionToolbarIconButton: ToolbarButton {
|
||||
id: iconBtn
|
||||
implicitWidth: height
|
||||
|
||||
colBackgroundToggled: Appearance.colors.colSecondaryContainer
|
||||
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
|
||||
colRippleToggled: Appearance.colors.colSecondaryContainerActive
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
iconSize: 24
|
||||
text: iconBtn.text
|
||||
color: iconBtn.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
|
||||
}
|
||||
}
|
||||
|
||||
component IconAndTextPair: Row {
|
||||
id: pair
|
||||
required property string icon
|
||||
|
||||
@@ -159,7 +159,13 @@ Scope {
|
||||
}
|
||||
|
||||
Item { // No player placeholder
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: {
|
||||
if (mediaControlsRoot.anchors.left) return Qt.AlignLeft;
|
||||
if (mediaControlsRoot.anchors.right) return Qt.AlignRight;
|
||||
return Qt.AlignHCenter;
|
||||
}
|
||||
Layout.leftMargin: Appearance.sizes.hyprlandGapsOut
|
||||
Layout.rightMargin: Appearance.sizes.hyprlandGapsOut
|
||||
visible: root.meaningfulPlayers.length === 0
|
||||
implicitWidth: placeholderBackground.implicitWidth + Appearance.sizes.elevationMargin
|
||||
implicitHeight: placeholderBackground.implicitHeight + Appearance.sizes.elevationMargin
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
import "../"
|
||||
import qs.modules.onScreenDisplay
|
||||
|
||||
OsdValueIndicator {
|
||||
id: root
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import "../"
|
||||
import qs.modules.onScreenDisplay
|
||||
|
||||
OsdValueIndicator {
|
||||
id: osdValues
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import Quickshell
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property color color
|
||||
required property color overlayColor
|
||||
required property list<point> points
|
||||
property int strokeWidth: Config.options.regionSelector.circle.strokeWidth
|
||||
|
||||
function updatePoints() {
|
||||
if (!root.dragging) return;
|
||||
root.points.push({ x: root.mouseX, y: root.mouseY });
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
id: darkenOverlay
|
||||
z: 1
|
||||
anchors.fill: parent
|
||||
color: root.overlayColor
|
||||
}
|
||||
|
||||
Shape {
|
||||
id: shape
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
layer.enabled: true
|
||||
layer.smooth: true
|
||||
preferredRendererType: Shape.CurveRenderer
|
||||
|
||||
ShapePath {
|
||||
id: shapePath
|
||||
strokeWidth: root.strokeWidth
|
||||
pathHints: ShapePath.PathLinear
|
||||
fillColor: "transparent"
|
||||
strokeColor: root.color
|
||||
capStyle: ShapePath.RoundCap
|
||||
joinStyle: ShapePath.RoundJoin
|
||||
|
||||
PathPolyline {
|
||||
path: root.points
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import QtQuick
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property real regionX
|
||||
required property real regionY
|
||||
required property real regionWidth
|
||||
required property real regionHeight
|
||||
required property real mouseX
|
||||
required property real mouseY
|
||||
required property color color
|
||||
required property color overlayColor
|
||||
property bool showAimLines: Config.options.regionSelector.rect.showAimLines
|
||||
|
||||
// Overlay to darken screen
|
||||
// Base dark overlay around region
|
||||
Rectangle {
|
||||
id: darkenOverlay
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: root.regionX - darkenOverlay.border.width
|
||||
topMargin: root.regionY - darkenOverlay.border.width
|
||||
}
|
||||
width: root.regionWidth + darkenOverlay.border.width * 2
|
||||
height: root.regionHeight + darkenOverlay.border.width * 2
|
||||
color: "transparent"
|
||||
border.color: root.overlayColor
|
||||
border.width: Math.max(root.width, root.height)
|
||||
}
|
||||
|
||||
// Selection border
|
||||
Rectangle {
|
||||
id: selectionBorder
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: root.regionX
|
||||
topMargin: root.regionY
|
||||
}
|
||||
width: root.regionWidth
|
||||
height: root.regionHeight
|
||||
color: "transparent"
|
||||
border.color: root.color
|
||||
border.width: 2
|
||||
// radius: root.standardRounding
|
||||
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
|
||||
}
|
||||
|
||||
StyledText {
|
||||
z: 2
|
||||
anchors {
|
||||
top: selectionBorder.bottom
|
||||
right: selectionBorder.right
|
||||
margins: 8
|
||||
}
|
||||
color: root.color
|
||||
text: `${Math.round(root.regionWidth)} x ${Math.round(root.regionHeight)}`
|
||||
}
|
||||
|
||||
// Coord lines
|
||||
Rectangle { // Vertical
|
||||
visible: root.showAimLines
|
||||
opacity: 0.2
|
||||
z: 2
|
||||
x: root.mouseX
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
}
|
||||
width: 1
|
||||
color: root.color
|
||||
}
|
||||
Rectangle { // Horizontal
|
||||
visible: root.showAimLines
|
||||
opacity: 0.2
|
||||
z: 2
|
||||
y: root.mouseY
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
height: 1
|
||||
color: root.color
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Widgets
|
||||
import Quickshell.Hyprland
|
||||
|
||||
PanelWindow {
|
||||
id: root
|
||||
visible: false
|
||||
WlrLayershell.namespace: "quickshell:regionSelector"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
// TODO: Ask: sidebar AI; Ocr: tesseract
|
||||
enum SnipAction { Copy, Edit, Search }
|
||||
enum SelectionMode { RectCorners, Circle }
|
||||
property var action: RegionSelection.SnipAction.Copy
|
||||
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
signal dismiss()
|
||||
|
||||
property string screenshotDir: Directories.screenshotTemp
|
||||
property string imageSearchEngineBaseUrl: Config.options.search.imageSearch.imageSearchEngineBaseUrl
|
||||
property string fileUploadApiEndpoint: "https://uguu.se/upload"
|
||||
property color overlayColor: "#88111111"
|
||||
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
|
||||
property color genericContentForeground: "#ddffffff"
|
||||
property color brightText: Appearance.m3colors.darkmode ? Appearance.colors.colOnLayer0 : Appearance.colors.colLayer0
|
||||
property color brightSecondary: Appearance.m3colors.darkmode ? Appearance.colors.colSecondary : Appearance.colors.colOnSecondary
|
||||
property color brightTertiary: Appearance.m3colors.darkmode ? Appearance.colors.colTertiary : Qt.lighter(Appearance.colors.colPrimary)
|
||||
property color selectionBorderColor: ColorUtils.mix(brightText, brightSecondary, 0.5)
|
||||
property color selectionFillColor: "#33ffffff"
|
||||
property color windowBorderColor: brightSecondary
|
||||
property color windowFillColor: ColorUtils.transparentize(windowBorderColor, 0.85)
|
||||
property color imageBorderColor: brightTertiary
|
||||
property color imageFillColor: ColorUtils.transparentize(imageBorderColor, 0.85)
|
||||
property color onBorderColor: "#ff000000"
|
||||
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
|
||||
// Sort floating=true windows before others
|
||||
if (a.floating === b.floating) return 0;
|
||||
return a.floating ? -1 : 1;
|
||||
})
|
||||
readonly property var layers: HyprlandData.layers
|
||||
readonly property real falsePositivePreventionRatio: 0.5
|
||||
|
||||
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
|
||||
readonly property real monitorScale: hyprlandMonitor.scale
|
||||
readonly property real monitorOffsetX: hyprlandMonitor.x
|
||||
readonly property real monitorOffsetY: hyprlandMonitor.y
|
||||
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
|
||||
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
|
||||
property real dragStartX: 0
|
||||
property real dragStartY: 0
|
||||
property real draggingX: 0
|
||||
property real draggingY: 0
|
||||
property real dragDiffX: 0
|
||||
property real dragDiffY: 0
|
||||
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
|
||||
property bool dragging: false
|
||||
property list<point> points: []
|
||||
property var mouseButton: null
|
||||
property var imageRegions: []
|
||||
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
|
||||
root.windows.filter(w => w.workspace.id === root.activeWorkspaceId),
|
||||
root.layerRegions
|
||||
).map(window => {
|
||||
return {
|
||||
at: [window.at[0] - root.monitorOffsetX, window.at[1] - root.monitorOffsetY],
|
||||
size: [window.size[0], window.size[1]],
|
||||
class: window.class,
|
||||
title: window.title,
|
||||
}
|
||||
})
|
||||
readonly property list<var> layerRegions: {
|
||||
const layersOfThisMonitor = root.layers[root.hyprlandMonitor.name]
|
||||
const topLayers = layersOfThisMonitor?.levels["2"]
|
||||
if (!topLayers) return [];
|
||||
const nonBarTopLayers = topLayers
|
||||
.filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock")))
|
||||
.map(layer => {
|
||||
return {
|
||||
at: [layer.x, layer.y],
|
||||
size: [layer.w, layer.h],
|
||||
namespace: layer.namespace,
|
||||
}
|
||||
})
|
||||
const offsetAdjustedLayers = nonBarTopLayers.map(layer => {
|
||||
return {
|
||||
at: [layer.at[0] - root.monitorOffsetX, layer.at[1] - root.monitorOffsetY],
|
||||
size: layer.size,
|
||||
namespace: layer.namespace,
|
||||
}
|
||||
});
|
||||
return offsetAdjustedLayers;
|
||||
}
|
||||
|
||||
property bool isCircleSelection: (root.selectionMode === RegionSelection.SelectionMode.Circle)
|
||||
property bool enableWindowRegions: Config.options.regionSelector.targetRegions.windows && !isCircleSelection
|
||||
property bool enableLayerRegions: Config.options.regionSelector.targetRegions.layers && !isCircleSelection
|
||||
property bool enableContentRegions: Config.options.regionSelector.targetRegions.content
|
||||
property real targetRegionOpacity: Config.options.regionSelector.targetRegions.opacity
|
||||
property bool contentRegionOpacity: Config.options.regionSelector.targetRegions.contentRegionOpacity
|
||||
|
||||
property real targetedRegionX: -1
|
||||
property real targetedRegionY: -1
|
||||
property real targetedRegionWidth: 0
|
||||
property real targetedRegionHeight: 0
|
||||
function targetedRegionValid() {
|
||||
return (root.targetedRegionX >= 0 && root.targetedRegionY >= 0)
|
||||
}
|
||||
function setRegionToTargeted() {
|
||||
root.regionX = root.targetedRegionX;
|
||||
root.regionY = root.targetedRegionY;
|
||||
root.regionWidth = root.targetedRegionWidth;
|
||||
root.regionHeight = root.targetedRegionHeight;
|
||||
}
|
||||
|
||||
function intersectionOverUnion(regionA, regionB) {
|
||||
// region: { at: [x, y], size: [w, h] }
|
||||
const ax1 = regionA.at[0], ay1 = regionA.at[1];
|
||||
const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1];
|
||||
const bx1 = regionB.at[0], by1 = regionB.at[1];
|
||||
const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1];
|
||||
|
||||
const interX1 = Math.max(ax1, bx1);
|
||||
const interY1 = Math.max(ay1, by1);
|
||||
const interX2 = Math.min(ax2, bx2);
|
||||
const interY2 = Math.min(ay2, by2);
|
||||
|
||||
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
||||
const areaA = (ax2 - ax1) * (ay2 - ay1);
|
||||
const areaB = (bx2 - bx1) * (by2 - by1);
|
||||
const unionArea = areaA + areaB - interArea;
|
||||
|
||||
return unionArea > 0 ? interArea / unionArea : 0;
|
||||
}
|
||||
|
||||
function filterOverlappingImageRegions(regions) {
|
||||
let keep = [];
|
||||
let removed = new Set();
|
||||
for (let i = 0; i < regions.length; ++i) {
|
||||
if (removed.has(i)) continue;
|
||||
let regionA = regions[i];
|
||||
for (let j = i + 1; j < regions.length; ++j) {
|
||||
if (removed.has(j)) continue;
|
||||
let regionB = regions[j];
|
||||
if (intersectionOverUnion(regionA, regionB) > 0) {
|
||||
// Compare areas
|
||||
let areaA = regionA.size[0] * regionA.size[1];
|
||||
let areaB = regionB.size[0] * regionB.size[1];
|
||||
if (areaA <= areaB) {
|
||||
removed.add(j);
|
||||
} else {
|
||||
removed.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < regions.length; ++i) {
|
||||
if (!removed.has(i)) keep.push(regions[i]);
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
|
||||
function filterWindowRegionsByLayers(windowRegions, layerRegions) {
|
||||
return windowRegions.filter(windowRegion => {
|
||||
for (let i = 0; i < layerRegions.length; ++i) {
|
||||
if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function filterImageRegions(regions, windowRegions, threshold = 0.1) {
|
||||
// Remove image regions that overlap too much with any window region
|
||||
let filtered = regions.filter(region => {
|
||||
for (let i = 0; i < windowRegions.length; ++i) {
|
||||
if (intersectionOverUnion(region, windowRegions[i]) > threshold)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Remove overlapping image regions, keep only the smaller one
|
||||
return filterOverlappingImageRegions(filtered);
|
||||
}
|
||||
|
||||
function updateTargetedRegion(x, y) {
|
||||
// Image regions
|
||||
const clickedRegion = root.imageRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedRegion) {
|
||||
root.targetedRegionX = clickedRegion.at[0];
|
||||
root.targetedRegionY = clickedRegion.at[1];
|
||||
root.targetedRegionWidth = clickedRegion.size[0];
|
||||
root.targetedRegionHeight = clickedRegion.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
// Layer regions
|
||||
const clickedLayer = root.layerRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedLayer) {
|
||||
root.targetedRegionX = clickedLayer.at[0];
|
||||
root.targetedRegionY = clickedLayer.at[1];
|
||||
root.targetedRegionWidth = clickedLayer.size[0];
|
||||
root.targetedRegionHeight = clickedLayer.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
// Window regions
|
||||
const clickedWindow = root.windowRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedWindow) {
|
||||
root.targetedRegionX = clickedWindow.at[0];
|
||||
root.targetedRegionY = clickedWindow.at[1];
|
||||
root.targetedRegionWidth = clickedWindow.size[0];
|
||||
root.targetedRegionHeight = clickedWindow.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
root.targetedRegionX = -1;
|
||||
root.targetedRegionY = -1;
|
||||
root.targetedRegionWidth = 0;
|
||||
root.targetedRegionHeight = 0;
|
||||
}
|
||||
|
||||
property real regionWidth: Math.abs(draggingX - dragStartX)
|
||||
property real regionHeight: Math.abs(draggingY - dragStartY)
|
||||
property real regionX: Math.min(dragStartX, draggingX)
|
||||
property real regionY: Math.min(dragStartY, draggingY)
|
||||
|
||||
Process {
|
||||
id: screenshotProcess
|
||||
running: true
|
||||
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(root.screen.name)}' '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
root.visible = true;
|
||||
imageDetectionProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: imageDetectionProcess
|
||||
command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh `
|
||||
+ `--hyprctl `
|
||||
+ `--image '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}' `
|
||||
+ `--max-width ${Math.round(root.screen.width * root.falsePositivePreventionRatio)} `
|
||||
+ `--max-height ${Math.round(root.screen.height * root.falsePositivePreventionRatio)} `]
|
||||
stdout: StdioCollector {
|
||||
id: imageDimensionCollector
|
||||
onStreamFinished: {
|
||||
imageRegions = filterImageRegions(
|
||||
JSON.parse(imageDimensionCollector.text),
|
||||
root.windowRegions
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function snip() {
|
||||
// Validity check
|
||||
if (root.regionWidth <= 0 || root.regionHeight <= 0) {
|
||||
console.warn("[Region Selector] Invalid region size, skipping snip.");
|
||||
root.dismiss();
|
||||
}
|
||||
|
||||
// Clamp region to screen bounds
|
||||
root.regionX = Math.max(0, Math.min(root.regionX, root.screen.width - root.regionWidth));
|
||||
root.regionY = Math.max(0, Math.min(root.regionY, root.screen.height - root.regionHeight));
|
||||
root.regionWidth = Math.max(0, Math.min(root.regionWidth, root.screen.width - root.regionX));
|
||||
root.regionHeight = Math.max(0, Math.min(root.regionHeight, root.screen.height - root.regionY));
|
||||
|
||||
// Adjust action
|
||||
if (root.action === RegionSelection.SnipAction.Copy || root.action === RegionSelection.SnipAction.Edit) {
|
||||
root.action = root.mouseButton === Qt.RightButton ? RegionSelection.SnipAction.Edit : RegionSelection.SnipAction.Copy;
|
||||
}
|
||||
|
||||
// Set command for action
|
||||
const cropBase = `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} `
|
||||
+ `-crop ${root.regionWidth * root.monitorScale}x${root.regionHeight * root.monitorScale}+${root.regionX * root.monitorScale}+${root.regionY * root.monitorScale}`
|
||||
const cropToStdout = `${cropBase} -`
|
||||
const cropInPlace = `${cropBase} '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
|
||||
const cleanup = `rm '${StringUtils.shellSingleQuoteEscape(root.screenshotPath)}'`
|
||||
const uploadAndGetUrl = (filePath) => {
|
||||
return `curl -sF files[]=@'${StringUtils.shellSingleQuoteEscape(filePath)}' ${root.fileUploadApiEndpoint} | jq -r '.files[0].url'`
|
||||
}
|
||||
switch (root.action) {
|
||||
case RegionSelection.SnipAction.Copy:
|
||||
snipProc.command = ["bash", "-c", `${cropToStdout} | wl-copy && ${cleanup}`]
|
||||
break;
|
||||
case RegionSelection.SnipAction.Edit:
|
||||
snipProc.command = ["bash", "-c", `${cropToStdout} | swappy -f - && ${cleanup}`]
|
||||
break;
|
||||
case RegionSelection.SnipAction.Search:
|
||||
snipProc.command = ["bash", "-c", `${cropInPlace} && xdg-open "${root.imageSearchEngineBaseUrl}$(${uploadAndGetUrl(root.screenshotPath)})"`]
|
||||
break;
|
||||
default:
|
||||
console.warn("[Region Selector] Unknown snip action, skipping snip.");
|
||||
root.dismiss();
|
||||
return;
|
||||
}
|
||||
|
||||
// Image post-processing
|
||||
snipProc.startDetached();
|
||||
root.dismiss();
|
||||
}
|
||||
|
||||
Process {
|
||||
id: snipProc
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
anchors.fill: parent
|
||||
live: false
|
||||
captureSource: root.screen
|
||||
|
||||
focus: root.visible
|
||||
Keys.onPressed: (event) => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.CrossCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
|
||||
// Controls
|
||||
onPressed: (mouse) => {
|
||||
root.dragStartX = mouse.x;
|
||||
root.dragStartY = mouse.y;
|
||||
root.draggingX = mouse.x;
|
||||
root.draggingY = mouse.y;
|
||||
root.dragging = true;
|
||||
root.mouseButton = mouse.button;
|
||||
}
|
||||
onReleased: (mouse) => {
|
||||
// Circle dragging?
|
||||
if (root.selectionMode === RegionSelection.SelectionMode.Circle) {
|
||||
const padding = Config.options.regionSelector.circle.padding + Config.options.regionSelector.circle.strokeWidth / 2;
|
||||
const dragPoints = (root.points.length > 0) ? root.points : [{ x: mouseArea.mouseX, y: mouseArea.mouseY }];
|
||||
const maxX = Math.max(...dragPoints.map(p => p.x));
|
||||
const minX = Math.min(...dragPoints.map(p => p.x));
|
||||
const maxY = Math.max(...dragPoints.map(p => p.y));
|
||||
const minY = Math.min(...dragPoints.map(p => p.y));
|
||||
root.regionX = minX - padding;
|
||||
root.regionY = minY - padding;
|
||||
root.regionWidth = maxX - minX + padding * 2;
|
||||
root.regionHeight = maxY - minY + padding * 2;
|
||||
if (root.targetedRegionValid() && imageRegions.find(region => {
|
||||
return (region.at[0] === root.targetedRegionX
|
||||
&& region.at[1] === root.targetedRegionY
|
||||
&& region.size[0] === root.targetedRegionWidth
|
||||
&& region.size[1] === root.targetedRegionHeight)
|
||||
})) {
|
||||
root.setRegionToTargeted();
|
||||
}
|
||||
}
|
||||
// Detect if it was a click -> Try to select targeted region
|
||||
else if (root.draggingX === root.dragStartX && root.draggingY === root.dragStartY) {
|
||||
if (root.targetedRegionValid()) {
|
||||
root.setRegionToTargeted();
|
||||
}
|
||||
}
|
||||
root.snip();
|
||||
}
|
||||
onPositionChanged: (mouse) => {
|
||||
root.updateTargetedRegion(mouse.x, mouse.y);
|
||||
if (!root.dragging) return;
|
||||
root.draggingX = mouse.x;
|
||||
root.draggingY = mouse.y;
|
||||
root.dragDiffX = mouse.x - root.dragStartX;
|
||||
root.dragDiffY = mouse.y - root.dragStartY;
|
||||
root.points.push({ x: mouse.x, y: mouse.y });
|
||||
}
|
||||
|
||||
Loader {
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
active: root.selectionMode === RegionSelection.SelectionMode.RectCorners
|
||||
sourceComponent: RectCornersSelectionDetails {
|
||||
regionX: root.regionX
|
||||
regionY: root.regionY
|
||||
regionWidth: root.regionWidth
|
||||
regionHeight: root.regionHeight
|
||||
mouseX: mouseArea.mouseX
|
||||
mouseY: mouseArea.mouseY
|
||||
color: root.selectionBorderColor
|
||||
overlayColor: root.overlayColor
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
z: 2
|
||||
anchors.fill: parent
|
||||
active: root.selectionMode === RegionSelection.SelectionMode.Circle
|
||||
sourceComponent: CircleSelectionDetails {
|
||||
color: root.selectionBorderColor
|
||||
overlayColor: root.overlayColor
|
||||
points: root.points
|
||||
}
|
||||
}
|
||||
|
||||
// Window regions
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.enableWindowRegions ? root.windowRegions : []
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
z: 2
|
||||
required property var modelData
|
||||
showIcon: true
|
||||
targeted: !root.draggedAway &&
|
||||
(root.targetedRegionX === modelData.at[0]
|
||||
&& root.targetedRegionY === modelData.at[1]
|
||||
&& root.targetedRegionWidth === modelData.size[0]
|
||||
&& root.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
colBackground: root.genericContentColor
|
||||
colForeground: root.genericContentForeground
|
||||
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.windowBorderColor
|
||||
fillColor: targeted ? root.windowFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: `${modelData.class}`
|
||||
radius: Appearance.rounding.windowRounding
|
||||
}
|
||||
}
|
||||
|
||||
// Layer regions
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.enableLayerRegions ? root.layerRegions : []
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
z: 3
|
||||
required property var modelData
|
||||
targeted: !root.draggedAway &&
|
||||
(root.targetedRegionX === modelData.at[0]
|
||||
&& root.targetedRegionY === modelData.at[1]
|
||||
&& root.targetedRegionWidth === modelData.size[0]
|
||||
&& root.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
colBackground: root.genericContentColor
|
||||
colForeground: root.genericContentForeground
|
||||
opacity: root.draggedAway ? 0 : root.targetRegionOpacity
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.windowBorderColor
|
||||
fillColor: targeted ? root.windowFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: `${modelData.namespace}`
|
||||
radius: Appearance.rounding.windowRounding
|
||||
}
|
||||
}
|
||||
|
||||
// Content regions
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.enableContentRegions ? root.imageRegions : []
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
z: 4
|
||||
required property var modelData
|
||||
targeted: !root.draggedAway &&
|
||||
(root.targetedRegionX === modelData.at[0]
|
||||
&& root.targetedRegionY === modelData.at[1]
|
||||
&& root.targetedRegionWidth === modelData.size[0]
|
||||
&& root.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
colBackground: root.genericContentColor
|
||||
colForeground: root.genericContentForeground
|
||||
opacity: root.draggedAway ? 0 : root.contentRegionOpacity
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.imageBorderColor
|
||||
fillColor: targeted ? root.imageFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: Translation.tr("Content region")
|
||||
}
|
||||
}
|
||||
|
||||
// Options toolbar
|
||||
Toolbar {
|
||||
id: toolbar
|
||||
z: 9999
|
||||
anchors {
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
bottom: parent.bottom
|
||||
bottomMargin: -height
|
||||
}
|
||||
opacity: 0
|
||||
Connections {
|
||||
target: root
|
||||
function onVisibleChanged() {
|
||||
if (!visible) return;
|
||||
toolbar.anchors.bottomMargin = 8;
|
||||
toolbar.opacity = 1;
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on anchors.bottomMargin {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
MaterialCookie {
|
||||
Layout.fillHeight: true
|
||||
Layout.leftMargin: 2
|
||||
Layout.rightMargin: 2
|
||||
implicitSize: 36 // Intentionally smaller because this one is brighter than others
|
||||
sides: 10
|
||||
amplitude: implicitSize / 44
|
||||
color: Appearance.colors.colPrimary
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: 22
|
||||
color: Appearance.colors.colOnPrimary
|
||||
animateChange: true
|
||||
text: switch (root.action) {
|
||||
case RegionSelection.SnipAction.Copy:
|
||||
case RegionSelection.SnipAction.Edit:
|
||||
return "content_cut";
|
||||
case RegionSelection.SnipAction.Search:
|
||||
return "image_search";
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
IconAndTextToolbarButton {
|
||||
iconText: "activity_zone"
|
||||
text: Translation.tr("Rect")
|
||||
toggled: root.selectionMode === RegionSelection.SelectionMode.RectCorners
|
||||
onClicked: root.selectionMode = RegionSelection.SelectionMode.RectCorners
|
||||
}
|
||||
|
||||
IconAndTextToolbarButton {
|
||||
iconText: "gesture"
|
||||
text: Translation.tr("Circle")
|
||||
toggled: root.selectionMode === RegionSelection.SelectionMode.Circle
|
||||
onClicked: root.selectionMode = RegionSelection.SelectionMode.Circle
|
||||
}
|
||||
|
||||
IconToolbarButton {
|
||||
text: "close"
|
||||
colBackground: Appearance.colors.colLayer3
|
||||
onClicked: root.dismiss();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,83 +16,13 @@ import Quickshell.Hyprland
|
||||
|
||||
Scope {
|
||||
id: root
|
||||
property string screenshotDir: Directories.screenshotTemp
|
||||
property color overlayColor: "#77111111"
|
||||
property color genericContentColor: Qt.alpha(root.overlayColor, 0.9)
|
||||
property color genericContentForeground: "#ddffffff"
|
||||
property color selectionBorderColor: "#ddf1f1f1"
|
||||
property color selectionFillColor: "#33ffffff"
|
||||
property color windowBorderColor: "#dda0c0da"
|
||||
property color windowFillColor: "#22a0c0da"
|
||||
property color imageBorderColor: "#ddf1d1ff"
|
||||
property color imageFillColor: "#33f1d1ff"
|
||||
property color onBorderColor: "#ff000000"
|
||||
property real standardRounding: 4
|
||||
readonly property var windows: [...HyprlandData.windowList].sort((a, b) => {
|
||||
// Sort floating=true windows before others
|
||||
if (a.floating === b.floating) return 0;
|
||||
return a.floating ? -1 : 1;
|
||||
})
|
||||
readonly property var layers: HyprlandData.layers
|
||||
readonly property real falsePositivePreventionRatio: 0.5
|
||||
|
||||
function dismiss() {
|
||||
GlobalStates.regionSelectorOpen = false
|
||||
}
|
||||
|
||||
component TargetRegion: Rectangle {
|
||||
id: regionRect
|
||||
property bool showIcon: false
|
||||
property bool targeted: false
|
||||
property color borderColor
|
||||
property color fillColor: "transparent"
|
||||
property string text: ""
|
||||
property real textPadding: 10
|
||||
z: 2
|
||||
color: fillColor
|
||||
border.color: borderColor
|
||||
border.width: targeted ? 3 : 1
|
||||
radius: root.standardRounding
|
||||
|
||||
Rectangle {
|
||||
id: regionLabelBackground
|
||||
property real verticalPadding: 5
|
||||
property real horizontalPadding: 10
|
||||
radius: 10
|
||||
color: root.genericContentColor
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
topMargin: regionRect.textPadding
|
||||
leftMargin: regionRect.textPadding
|
||||
}
|
||||
implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
|
||||
Row {
|
||||
id: regionInfoRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Loader {
|
||||
id: regionIconLoader
|
||||
active: regionRect.showIcon
|
||||
visible: active
|
||||
sourceComponent: IconImage {
|
||||
implicitSize: Appearance.font.pixelSize.larger
|
||||
source: Quickshell.iconPath(AppSearch.guessIcon(regionRect.text), "image-missing")
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: regionText
|
||||
text: regionRect.text
|
||||
color: root.genericContentForeground
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
property var action: RegionSelection.SnipAction.Copy
|
||||
property var selectionMode: RegionSelection.SelectionMode.RectCorners
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
@@ -101,478 +31,28 @@ Scope {
|
||||
required property var modelData
|
||||
active: GlobalStates.regionSelectorOpen
|
||||
|
||||
sourceComponent: PanelWindow {
|
||||
id: panelWindow
|
||||
sourceComponent: RegionSelection {
|
||||
screen: regionSelectorLoader.modelData
|
||||
visible: false
|
||||
WlrLayershell.namespace: "quickshell:regionSelector"
|
||||
WlrLayershell.layer: WlrLayer.Overlay
|
||||
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
|
||||
exclusionMode: ExclusionMode.Ignore
|
||||
anchors {
|
||||
left: true
|
||||
right: true
|
||||
top: true
|
||||
bottom: true
|
||||
}
|
||||
|
||||
readonly property HyprlandMonitor hyprlandMonitor: Hyprland.monitorFor(screen)
|
||||
readonly property real monitorScale: hyprlandMonitor.scale
|
||||
readonly property real monitorOffsetX: hyprlandMonitor.x
|
||||
readonly property real monitorOffsetY: hyprlandMonitor.y
|
||||
property int activeWorkspaceId: hyprlandMonitor.activeWorkspace?.id ?? 0
|
||||
property string screenshotPath: `${root.screenshotDir}/image-${screen.name}`
|
||||
property real dragStartX: 0
|
||||
property real dragStartY: 0
|
||||
property real draggingX: 0
|
||||
property real draggingY: 0
|
||||
property real dragDiffX: 0
|
||||
property real dragDiffY: 0
|
||||
property bool draggedAway: (dragDiffX !== 0 || dragDiffY !== 0)
|
||||
property bool dragging: false
|
||||
property var mouseButton: null
|
||||
property var imageRegions: []
|
||||
readonly property list<var> windowRegions: filterWindowRegionsByLayers(
|
||||
root.windows.filter(w => w.workspace.id === panelWindow.activeWorkspaceId),
|
||||
panelWindow.layerRegions
|
||||
).map(window => {
|
||||
return {
|
||||
at: [window.at[0] - panelWindow.monitorOffsetX, window.at[1] - panelWindow.monitorOffsetY],
|
||||
size: [window.size[0], window.size[1]],
|
||||
class: window.class,
|
||||
title: window.title,
|
||||
}
|
||||
})
|
||||
readonly property list<var> layerRegions: {
|
||||
const layersOfThisMonitor = root.layers[panelWindow.hyprlandMonitor.name]
|
||||
const topLayers = layersOfThisMonitor?.levels["2"]
|
||||
if (!topLayers) return [];
|
||||
const nonBarTopLayers = topLayers
|
||||
.filter(layer => !(layer.namespace.includes(":bar") || layer.namespace.includes(":verticalBar") || layer.namespace.includes(":dock")))
|
||||
.map(layer => {
|
||||
return {
|
||||
at: [layer.x, layer.y],
|
||||
size: [layer.w, layer.h],
|
||||
namespace: layer.namespace,
|
||||
}
|
||||
})
|
||||
const offsetAdjustedLayers = nonBarTopLayers.map(layer => {
|
||||
return {
|
||||
at: [layer.at[0] - panelWindow.monitorOffsetX, layer.at[1] - panelWindow.monitorOffsetY],
|
||||
size: layer.size,
|
||||
namespace: layer.namespace,
|
||||
}
|
||||
});
|
||||
return offsetAdjustedLayers;
|
||||
}
|
||||
|
||||
property real targetedRegionX: -1
|
||||
property real targetedRegionY: -1
|
||||
property real targetedRegionWidth: 0
|
||||
property real targetedRegionHeight: 0
|
||||
|
||||
function intersectionOverUnion(regionA, regionB) {
|
||||
// region: { at: [x, y], size: [w, h] }
|
||||
const ax1 = regionA.at[0], ay1 = regionA.at[1];
|
||||
const ax2 = ax1 + regionA.size[0], ay2 = ay1 + regionA.size[1];
|
||||
const bx1 = regionB.at[0], by1 = regionB.at[1];
|
||||
const bx2 = bx1 + regionB.size[0], by2 = by1 + regionB.size[1];
|
||||
|
||||
const interX1 = Math.max(ax1, bx1);
|
||||
const interY1 = Math.max(ay1, by1);
|
||||
const interX2 = Math.min(ax2, bx2);
|
||||
const interY2 = Math.min(ay2, by2);
|
||||
|
||||
const interArea = Math.max(0, interX2 - interX1) * Math.max(0, interY2 - interY1);
|
||||
const areaA = (ax2 - ax1) * (ay2 - ay1);
|
||||
const areaB = (bx2 - bx1) * (by2 - by1);
|
||||
const unionArea = areaA + areaB - interArea;
|
||||
|
||||
return unionArea > 0 ? interArea / unionArea : 0;
|
||||
}
|
||||
|
||||
function filterOverlappingImageRegions(regions) {
|
||||
let keep = [];
|
||||
let removed = new Set();
|
||||
for (let i = 0; i < regions.length; ++i) {
|
||||
if (removed.has(i)) continue;
|
||||
let regionA = regions[i];
|
||||
for (let j = i + 1; j < regions.length; ++j) {
|
||||
if (removed.has(j)) continue;
|
||||
let regionB = regions[j];
|
||||
if (intersectionOverUnion(regionA, regionB) > 0) {
|
||||
// Compare areas
|
||||
let areaA = regionA.size[0] * regionA.size[1];
|
||||
let areaB = regionB.size[0] * regionB.size[1];
|
||||
if (areaA <= areaB) {
|
||||
removed.add(j);
|
||||
} else {
|
||||
removed.add(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for (let i = 0; i < regions.length; ++i) {
|
||||
if (!removed.has(i)) keep.push(regions[i]);
|
||||
}
|
||||
return keep;
|
||||
}
|
||||
|
||||
function filterWindowRegionsByLayers(windowRegions, layerRegions) {
|
||||
return windowRegions.filter(windowRegion => {
|
||||
for (let i = 0; i < layerRegions.length; ++i) {
|
||||
if (intersectionOverUnion(windowRegion, layerRegions[i]) > 0)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
function filterImageRegions(regions, windowRegions, threshold = 0.1) {
|
||||
// Remove image regions that overlap too much with any window region
|
||||
let filtered = regions.filter(region => {
|
||||
for (let i = 0; i < windowRegions.length; ++i) {
|
||||
if (intersectionOverUnion(region, windowRegions[i]) > threshold)
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
// Remove overlapping image regions, keep only the smaller one
|
||||
return filterOverlappingImageRegions(filtered);
|
||||
}
|
||||
|
||||
function updateTargetedRegion(x, y) {
|
||||
// Image regions
|
||||
const clickedRegion = panelWindow.imageRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedRegion) {
|
||||
panelWindow.targetedRegionX = clickedRegion.at[0];
|
||||
panelWindow.targetedRegionY = clickedRegion.at[1];
|
||||
panelWindow.targetedRegionWidth = clickedRegion.size[0];
|
||||
panelWindow.targetedRegionHeight = clickedRegion.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
// Layer regions
|
||||
const clickedLayer = panelWindow.layerRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedLayer) {
|
||||
panelWindow.targetedRegionX = clickedLayer.at[0];
|
||||
panelWindow.targetedRegionY = clickedLayer.at[1];
|
||||
panelWindow.targetedRegionWidth = clickedLayer.size[0];
|
||||
panelWindow.targetedRegionHeight = clickedLayer.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
// Window regions
|
||||
const clickedWindow = panelWindow.windowRegions.find(region => {
|
||||
return region.at[0] <= x && x <= region.at[0] + region.size[0] && region.at[1] <= y && y <= region.at[1] + region.size[1];
|
||||
});
|
||||
if (clickedWindow) {
|
||||
panelWindow.targetedRegionX = clickedWindow.at[0];
|
||||
panelWindow.targetedRegionY = clickedWindow.at[1];
|
||||
panelWindow.targetedRegionWidth = clickedWindow.size[0];
|
||||
panelWindow.targetedRegionHeight = clickedWindow.size[1];
|
||||
return;
|
||||
}
|
||||
|
||||
panelWindow.targetedRegionX = -1;
|
||||
panelWindow.targetedRegionY = -1;
|
||||
panelWindow.targetedRegionWidth = 0;
|
||||
panelWindow.targetedRegionHeight = 0;
|
||||
}
|
||||
|
||||
property real regionWidth: Math.abs(draggingX - dragStartX)
|
||||
property real regionHeight: Math.abs(draggingY - dragStartY)
|
||||
property real regionX: Math.min(dragStartX, draggingX)
|
||||
property real regionY: Math.min(dragStartY, draggingY)
|
||||
|
||||
Process {
|
||||
id: screenshotProcess
|
||||
running: true
|
||||
command: ["bash", "-c", `mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}' && grim -o '${StringUtils.shellSingleQuoteEscape(panelWindow.screen.name)}' '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}'`]
|
||||
onExited: (exitCode, exitStatus) => {
|
||||
panelWindow.visible = true;
|
||||
imageDetectionProcess.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: imageDetectionProcess
|
||||
command: ["bash", "-c", `${Directories.scriptPath}/images/find-regions-venv.sh `
|
||||
+ `--hyprctl `
|
||||
+ `--image '${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)}' `
|
||||
+ `--max-width ${Math.round(panelWindow.screen.width * root.falsePositivePreventionRatio)} `
|
||||
+ `--max-height ${Math.round(panelWindow.screen.height * root.falsePositivePreventionRatio)} `]
|
||||
stdout: StdioCollector {
|
||||
id: imageDimensionCollector
|
||||
onStreamFinished: {
|
||||
imageRegions = filterImageRegions(
|
||||
JSON.parse(imageDimensionCollector.text),
|
||||
panelWindow.windowRegions
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: snipProc
|
||||
function snip() {
|
||||
if (panelWindow.regionWidth <= 0 || panelWindow.regionHeight <= 0) {
|
||||
console.warn("Invalid region size, skipping snip.");
|
||||
root.dismiss();
|
||||
}
|
||||
snipProc.startDetached();
|
||||
root.dismiss();
|
||||
}
|
||||
command: ["bash", "-c",
|
||||
`magick ${StringUtils.shellSingleQuoteEscape(panelWindow.screenshotPath)} `
|
||||
+ `-crop ${panelWindow.regionWidth * panelWindow.monitorScale}x${panelWindow.regionHeight * panelWindow.monitorScale}+${panelWindow.regionX * panelWindow.monitorScale}+${panelWindow.regionY * panelWindow.monitorScale} - `
|
||||
+ `| ${panelWindow.mouseButton === Qt.LeftButton ? "wl-copy" : "swappy -f -"}`]
|
||||
}
|
||||
|
||||
ScreencopyView {
|
||||
anchors.fill: parent
|
||||
live: false
|
||||
captureSource: panelWindow.screen
|
||||
|
||||
focus: panelWindow.visible
|
||||
Keys.onPressed: (event) => { // Esc to close
|
||||
if (event.key === Qt.Key_Escape) {
|
||||
root.dismiss();
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
cursorShape: Qt.CrossCursor
|
||||
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||
hoverEnabled: true
|
||||
|
||||
// Controls
|
||||
onPressed: mouse => {
|
||||
panelWindow.dragStartX = mouse.x;
|
||||
panelWindow.dragStartY = mouse.y;
|
||||
panelWindow.draggingX = mouse.x;
|
||||
panelWindow.draggingY = mouse.y;
|
||||
panelWindow.dragging = true;
|
||||
panelWindow.mouseButton = mouse.button;
|
||||
}
|
||||
onReleased: mouse => {
|
||||
// Detect if it was a click
|
||||
|
||||
// Image regions
|
||||
if (panelWindow.draggingX === panelWindow.dragStartX && panelWindow.draggingY === panelWindow.dragStartY) {
|
||||
if (panelWindow.targetedRegionX >= 0 && panelWindow.targetedRegionY >= 0) {
|
||||
panelWindow.regionX = panelWindow.targetedRegionX;
|
||||
panelWindow.regionY = panelWindow.targetedRegionY;
|
||||
panelWindow.regionWidth = panelWindow.targetedRegionWidth;
|
||||
panelWindow.regionHeight = panelWindow.targetedRegionHeight;
|
||||
}
|
||||
}
|
||||
snipProc.snip();
|
||||
}
|
||||
onPositionChanged: mouse => {
|
||||
if (panelWindow.dragging) {
|
||||
panelWindow.draggingX = mouse.x;
|
||||
panelWindow.draggingY = mouse.y;
|
||||
panelWindow.dragDiffX = mouse.x - panelWindow.dragStartX;
|
||||
panelWindow.dragDiffY = mouse.y - panelWindow.dragStartY;
|
||||
}
|
||||
panelWindow.updateTargetedRegion(mouse.x, mouse.y);
|
||||
}
|
||||
|
||||
// Overlay to darken screen
|
||||
Rectangle { // Base
|
||||
id: darkenOverlay
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: panelWindow.regionX - darkenOverlay.border.width
|
||||
topMargin: panelWindow.regionY - darkenOverlay.border.width
|
||||
}
|
||||
width: panelWindow.regionWidth + darkenOverlay.border.width * 2
|
||||
height: panelWindow.regionHeight + darkenOverlay.border.width * 2
|
||||
color: "transparent"
|
||||
// border.color: root.selectionBorderColor
|
||||
border.color: root.overlayColor
|
||||
border.width: Math.max(panelWindow.width, panelWindow.height)
|
||||
radius: root.standardRounding
|
||||
}
|
||||
Rectangle {
|
||||
id: selectionBorder
|
||||
z: 1
|
||||
anchors {
|
||||
left: parent.left
|
||||
top: parent.top
|
||||
leftMargin: panelWindow.regionX
|
||||
topMargin: panelWindow.regionY
|
||||
}
|
||||
width: panelWindow.regionWidth
|
||||
height: panelWindow.regionHeight
|
||||
color: "transparent"
|
||||
border.color: root.selectionBorderColor
|
||||
border.width: 2
|
||||
// radius: root.standardRounding
|
||||
radius: 0 // TODO: figure out how to make the overlay thing work with rounding
|
||||
}
|
||||
StyledText {
|
||||
z: 2
|
||||
anchors {
|
||||
top: selectionBorder.bottom
|
||||
right: selectionBorder.right
|
||||
margins: 8
|
||||
}
|
||||
color: root.selectionBorderColor
|
||||
text: `${Math.round(panelWindow.regionWidth)} x ${Math.round(panelWindow.regionHeight)}`
|
||||
}
|
||||
|
||||
// Instructions
|
||||
Rectangle {
|
||||
z: 9999
|
||||
anchors {
|
||||
top: parent.top
|
||||
horizontalCenter: parent.horizontalCenter
|
||||
topMargin: (Appearance.sizes.barHeight - implicitHeight) / 2
|
||||
}
|
||||
|
||||
opacity: panelWindow.dragging ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
color: root.genericContentColor
|
||||
radius: 10
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
implicitWidth: instructionsRow.implicitWidth + 10 * 2
|
||||
implicitHeight: instructionsRow.implicitHeight + 5 * 2
|
||||
|
||||
Row {
|
||||
id: instructionsRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
MaterialSymbol {
|
||||
id: screenshotRegionIcon
|
||||
// anchors.centerIn: parent
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
text: "screenshot_region"
|
||||
color: root.genericContentForeground
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
text: Translation.tr("Drag or click a region • LMB: Copy • RMB: Edit")
|
||||
color: root.genericContentForeground
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Window regions
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: panelWindow.windowRegions
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
z: 2
|
||||
required property var modelData
|
||||
showIcon: true
|
||||
targeted: !panelWindow.draggedAway &&
|
||||
(panelWindow.targetedRegionX === modelData.at[0]
|
||||
&& panelWindow.targetedRegionY === modelData.at[1]
|
||||
&& panelWindow.targetedRegionWidth === modelData.size[0]
|
||||
&& panelWindow.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
opacity: panelWindow.draggedAway ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.windowBorderColor
|
||||
fillColor: targeted ? root.windowFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: `${modelData.class}`
|
||||
radius: Appearance.rounding.windowRounding
|
||||
}
|
||||
}
|
||||
|
||||
// Layer regions
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: panelWindow.layerRegions
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
z: 3
|
||||
required property var modelData
|
||||
targeted: !panelWindow.draggedAway &&
|
||||
(panelWindow.targetedRegionX === modelData.at[0]
|
||||
&& panelWindow.targetedRegionY === modelData.at[1]
|
||||
&& panelWindow.targetedRegionWidth === modelData.size[0]
|
||||
&& panelWindow.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
opacity: panelWindow.draggedAway ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.windowBorderColor
|
||||
fillColor: targeted ? root.windowFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: `${modelData.namespace}`
|
||||
radius: Appearance.rounding.windowRounding
|
||||
}
|
||||
}
|
||||
|
||||
// Image regions
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: Config.options.screenshotTool.showContentRegions ? panelWindow.imageRegions : []
|
||||
}
|
||||
delegate: TargetRegion {
|
||||
z: 4
|
||||
required property var modelData
|
||||
targeted: !panelWindow.draggedAway &&
|
||||
(panelWindow.targetedRegionX === modelData.at[0]
|
||||
&& panelWindow.targetedRegionY === modelData.at[1]
|
||||
&& panelWindow.targetedRegionWidth === modelData.size[0]
|
||||
&& panelWindow.targetedRegionHeight === modelData.size[1])
|
||||
|
||||
opacity: panelWindow.draggedAway ? 0 : 1
|
||||
visible: opacity > 0
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
x: modelData.at[0]
|
||||
y: modelData.at[1]
|
||||
width: modelData.size[0]
|
||||
height: modelData.size[1]
|
||||
borderColor: root.imageBorderColor
|
||||
fillColor: targeted ? root.imageFillColor : "transparent"
|
||||
border.width: targeted ? 4 : 2
|
||||
text: "Content region"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onDismiss: root.dismiss()
|
||||
action: root.action
|
||||
selectionMode: root.selectionMode
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function screenshot() {
|
||||
root.action = RegionSelection.SnipAction.Copy
|
||||
root.selectionMode = RegionSelection.SelectionMode.RectCorners
|
||||
GlobalStates.regionSelectorOpen = true
|
||||
}
|
||||
|
||||
function search() {
|
||||
root.action = RegionSelection.SnipAction.Search
|
||||
if (Config.options.search.imageSearch.useCircleSelection) {
|
||||
root.selectionMode = RegionSelection.SelectionMode.Circle
|
||||
} else {
|
||||
root.selectionMode = RegionSelection.SelectionMode.RectCorners
|
||||
}
|
||||
GlobalStates.regionSelectorOpen = true
|
||||
}
|
||||
|
||||
@@ -582,14 +62,19 @@ Scope {
|
||||
function screenshot() {
|
||||
root.screenshot()
|
||||
}
|
||||
function search() {
|
||||
root.search()
|
||||
}
|
||||
}
|
||||
|
||||
GlobalShortcut {
|
||||
name: "regionScreenshot"
|
||||
description: "Takes a screenshot of the selected region"
|
||||
|
||||
onPressed: {
|
||||
root.screenshot()
|
||||
}
|
||||
onPressed: root.screenshot()
|
||||
}
|
||||
GlobalShortcut {
|
||||
name: "regionSearch"
|
||||
description: "Searches the selected region"
|
||||
onPressed: root.search()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
pragma ComponentBehavior: Bound
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Widgets
|
||||
|
||||
Rectangle {
|
||||
id: root
|
||||
required property color colBackground
|
||||
required property color colForeground
|
||||
property bool showLabel: Config.options.regionSelector.targetRegions.showLabel
|
||||
property bool showIcon: false
|
||||
property bool targeted: false
|
||||
property color borderColor
|
||||
property color fillColor: "transparent"
|
||||
property string text: ""
|
||||
property real textPadding: 10
|
||||
z: 2
|
||||
color: fillColor
|
||||
border.color: borderColor
|
||||
border.width: targeted ? 3 : 1
|
||||
radius: 4
|
||||
|
||||
Loader {
|
||||
anchors {
|
||||
top: parent.top
|
||||
left: parent.left
|
||||
topMargin: root.textPadding
|
||||
leftMargin: root.textPadding
|
||||
}
|
||||
|
||||
active: root.showLabel
|
||||
sourceComponent: Rectangle {
|
||||
property real verticalPadding: 5
|
||||
property real horizontalPadding: 10
|
||||
radius: 10
|
||||
color: root.colBackground
|
||||
border.width: 1
|
||||
border.color: Appearance.m3colors.m3outlineVariant
|
||||
implicitWidth: regionInfoRow.implicitWidth + horizontalPadding * 2
|
||||
implicitHeight: regionInfoRow.implicitHeight + verticalPadding * 2
|
||||
|
||||
Row {
|
||||
id: regionInfoRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 4
|
||||
|
||||
Loader {
|
||||
id: regionIconLoader
|
||||
active: root.showIcon
|
||||
visible: active
|
||||
sourceComponent: IconImage {
|
||||
implicitSize: Appearance.font.pixelSize.larger
|
||||
source: Quickshell.iconPath(AppSearch.guessIcon(root.text), "image-missing")
|
||||
}
|
||||
}
|
||||
|
||||
StyledText {
|
||||
id: regionText
|
||||
text: root.text
|
||||
color: root.colForeground
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,6 @@ Scope {
|
||||
id: sidebarCornerOpenInteractionLoader
|
||||
active: {
|
||||
if (!Config.options.sidebar.cornerOpen.enable) return false;
|
||||
if (!Config.options.bar.vertical && Config.options.sidebar.cornerOpen.bottom == Config.options.bar.bottom) return false;
|
||||
if (cornerPanelWindow.fullscreen) return false;
|
||||
return (Config.options.sidebar.cornerOpen.bottom == cornerWidget.isBottom);
|
||||
}
|
||||
|
||||
@@ -574,6 +574,100 @@ ContentPage {
|
||||
}
|
||||
}
|
||||
|
||||
ContentSection {
|
||||
icon: "screenshot_frame_2"
|
||||
title: Translation.tr("Region selector (screen snipping/Google Lens)")
|
||||
|
||||
ContentSubsection {
|
||||
title: Translation.tr("Hint target regions")
|
||||
ConfigRow {
|
||||
ConfigSwitch {
|
||||
buttonIcon: "select_window"
|
||||
text: Translation.tr('Windows')
|
||||
checked: Config.options.regionSelector.targetRegions.windows
|
||||
onCheckedChanged: {
|
||||
Config.options.regionSelector.targetRegions.windows = checked;
|
||||
}
|
||||
}
|
||||
ConfigSwitch {
|
||||
buttonIcon: "right_panel_open"
|
||||
text: Translation.tr('Layers')
|
||||
checked: Config.options.regionSelector.targetRegions.layers
|
||||
onCheckedChanged: {
|
||||
Config.options.regionSelector.targetRegions.layers = checked;
|
||||
}
|
||||
}
|
||||
ConfigSwitch {
|
||||
buttonIcon: "nearby"
|
||||
text: Translation.tr('Content')
|
||||
checked: Config.options.regionSelector.targetRegions.content
|
||||
onCheckedChanged: {
|
||||
Config.options.regionSelector.targetRegions.content = checked;
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentSubsection {
|
||||
title: Translation.tr("Google Lens")
|
||||
|
||||
ConfigSelectionArray {
|
||||
currentValue: Config.options.search.imageSearch.useCircleSelection ? "circle" : "rectangles"
|
||||
onSelected: newValue => {
|
||||
Config.options.search.imageSearch.useCircleSelection = (newValue === "circle");
|
||||
}
|
||||
options: [
|
||||
{ icon: "activity_zone", value: "rectangles", displayName: Translation.tr("Rectangular selection") },
|
||||
{ icon: "gesture", value: "circle", displayName: Translation.tr("Circle to Search") }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
ContentSubsection {
|
||||
title: Translation.tr("Rectangular selection")
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "point_scan"
|
||||
text: Translation.tr("Show aim lines")
|
||||
checked: Config.options.regionSelector.rect.showAimLines
|
||||
onCheckedChanged: {
|
||||
Config.options.regionSelector.rect.showAimLines = checked;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentSubsection {
|
||||
title: Translation.tr("Circle selection")
|
||||
|
||||
ConfigSpinBox {
|
||||
icon: "eraser_size_3"
|
||||
text: Translation.tr("Stroke width")
|
||||
value: Config.options.regionSelector.circle.strokeWidth
|
||||
from: 1
|
||||
to: 20
|
||||
stepSize: 1
|
||||
onValueChanged: {
|
||||
Config.options.regionSelector.circle.strokeWidth = value;
|
||||
}
|
||||
}
|
||||
|
||||
ConfigSpinBox {
|
||||
icon: "screenshot_frame_2"
|
||||
text: Translation.tr("Padding")
|
||||
value: Config.options.regionSelector.circle.padding
|
||||
from: 0
|
||||
to: 100
|
||||
stepSize: 5
|
||||
onValueChanged: {
|
||||
Config.options.regionSelector.circle.padding = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentSection {
|
||||
icon: "side_navigation"
|
||||
title: Translation.tr("Sidebars")
|
||||
@@ -848,23 +942,6 @@ ContentPage {
|
||||
}
|
||||
}
|
||||
|
||||
ContentSection {
|
||||
icon: "screenshot_frame_2"
|
||||
title: Translation.tr("Screenshot tool")
|
||||
|
||||
ConfigSwitch {
|
||||
buttonIcon: "nearby"
|
||||
text: Translation.tr('Show regions of potential interest')
|
||||
checked: Config.options.screenshotTool.showContentRegions
|
||||
onCheckedChanged: {
|
||||
Config.options.screenshotTool.showContentRegions = checked;
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Such regions could be images or parts of the screen that have some containment.\nMight not always be accurate.\nThis is done with an image processing algorithm run locally and no AI is used.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ContentSection {
|
||||
icon: "wallpaper_slideshow"
|
||||
title: Translation.tr("Wallpaper selector")
|
||||
|
||||
@@ -3,7 +3,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "./aiChat/"
|
||||
import qs.modules.sidebarLeft.aiChat
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
@@ -3,7 +3,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "./anime/"
|
||||
import qs.modules.sidebarLeft.anime
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
@@ -10,7 +10,6 @@ import Quickshell.Hyprland
|
||||
|
||||
Scope { // Scope
|
||||
id: root
|
||||
property int sidebarPadding: 15
|
||||
property bool detach: false
|
||||
property Component contentComponent: SidebarLeftContent {}
|
||||
property Item sidebarContent
|
||||
|
||||
@@ -9,6 +9,7 @@ import Qt5Compat.GraphicalEffects
|
||||
Item {
|
||||
id: root
|
||||
required property var scopeRoot
|
||||
property int sidebarPadding: 10
|
||||
anchors.fill: parent
|
||||
property bool aiChatEnabled: Config.options.policies.ai !== 0
|
||||
property bool translatorEnabled: Config.options.sidebar.translator.enable
|
||||
|
||||
@@ -2,7 +2,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "./translator/"
|
||||
import qs.modules.sidebarLeft.translator
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell
|
||||
|
||||
@@ -3,8 +3,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "../"
|
||||
import qs.services
|
||||
import qs.modules.sidebarLeft
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
@@ -287,4 +286,4 @@ Rectangle {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import "./calendar"
|
||||
import "./todo"
|
||||
import "./pomodoro"
|
||||
import qs.modules.sidebarRight.calendar
|
||||
import qs.modules.sidebarRight.todo
|
||||
import qs.modules.sidebarRight.pomodoro
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
@@ -248,4 +248,4 @@ Rectangle {
|
||||
anchors.margins: 5
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import "./notifications"
|
||||
import "./volumeMixer"
|
||||
import qs.modules.sidebarRight.notifications
|
||||
import qs.modules.sidebarRight.volumeMixer
|
||||
import Qt5Compat.GraphicalEffects
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
|
||||
@@ -9,21 +9,24 @@ import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
import Quickshell.Hyprland
|
||||
|
||||
import "./quickToggles/"
|
||||
import "./quickToggles/classicStyle/"
|
||||
import "./wifiNetworks/"
|
||||
import "./bluetoothDevices/"
|
||||
import "./volumeMixer/"
|
||||
import qs.modules.sidebarRight.quickToggles
|
||||
import qs.modules.sidebarRight.quickToggles.classicStyle
|
||||
|
||||
import qs.modules.sidebarRight.bluetoothDevices
|
||||
import qs.modules.sidebarRight.nightLight
|
||||
import qs.modules.sidebarRight.volumeMixer
|
||||
import qs.modules.sidebarRight.wifiNetworks
|
||||
|
||||
Item {
|
||||
id: root
|
||||
property int sidebarWidth: Appearance.sizes.sidebarWidth
|
||||
property int sidebarPadding: 12
|
||||
property int sidebarPadding: 10
|
||||
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
|
||||
property bool showWifiDialog: false
|
||||
property bool showBluetoothDialog: false
|
||||
property bool showAudioOutputDialog: false
|
||||
property bool showAudioInputDialog: false
|
||||
property bool showBluetoothDialog: false
|
||||
property bool showNightLightDialog: false
|
||||
property bool showWifiDialog: false
|
||||
property bool editMode: false
|
||||
|
||||
Connections {
|
||||
@@ -62,7 +65,8 @@ Item {
|
||||
|
||||
SystemButtonRow {
|
||||
Layout.fillHeight: false
|
||||
Layout.margins: 10
|
||||
Layout.fillWidth: true
|
||||
// Layout.margins: 10
|
||||
Layout.topMargin: 5
|
||||
Layout.bottomMargin: 0
|
||||
}
|
||||
@@ -108,18 +112,20 @@ Item {
|
||||
}
|
||||
|
||||
ToggleDialog {
|
||||
id: wifiDialogLoader
|
||||
shownPropertyString: "showWifiDialog"
|
||||
dialog: WifiDialog {}
|
||||
onShownChanged: {
|
||||
if (!shown) return;
|
||||
Network.enableWifi();
|
||||
Network.rescanWifi();
|
||||
shownPropertyString: "showAudioOutputDialog"
|
||||
dialog: VolumeDialog {
|
||||
isSink: true
|
||||
}
|
||||
}
|
||||
|
||||
ToggleDialog {
|
||||
shownPropertyString: "showAudioInputDialog"
|
||||
dialog: VolumeDialog {
|
||||
isSink: false
|
||||
}
|
||||
}
|
||||
|
||||
ToggleDialog {
|
||||
id: bluetoothDialogLoader
|
||||
shownPropertyString: "showBluetoothDialog"
|
||||
dialog: BluetoothDialog {}
|
||||
onShownChanged: {
|
||||
@@ -129,23 +135,21 @@ Item {
|
||||
Bluetooth.defaultAdapter.enabled = true;
|
||||
Bluetooth.defaultAdapter.discovering = true;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
ToggleDialog {
|
||||
id: audioOutputDialogLoader
|
||||
shownPropertyString: "showAudioOutputDialog"
|
||||
dialog: VolumeDialog {
|
||||
isSink: true
|
||||
}
|
||||
shownPropertyString: "showNightLightDialog"
|
||||
dialog: NightLightDialog {}
|
||||
}
|
||||
|
||||
ToggleDialog {
|
||||
id: audioInputDialogLoader
|
||||
shownPropertyString: "showAudioInputDialog"
|
||||
dialog: VolumeDialog {
|
||||
isSink: false
|
||||
shownPropertyString: "showWifiDialog"
|
||||
dialog: WifiDialog {}
|
||||
onShownChanged: {
|
||||
if (!shown) return;
|
||||
Network.enableWifi();
|
||||
Network.rescanWifi();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -185,45 +189,72 @@ Item {
|
||||
active: Config.options.sidebar.quickToggles.style === styleName
|
||||
Connections {
|
||||
target: quickPanelImplLoader.item
|
||||
function onOpenWifiDialog() {
|
||||
root.showWifiDialog = true;
|
||||
}
|
||||
function onOpenBluetoothDialog() {
|
||||
root.showBluetoothDialog = true;
|
||||
}
|
||||
function onOpenAudioOutputDialog() {
|
||||
root.showAudioOutputDialog = true;
|
||||
}
|
||||
function onOpenAudioInputDialog() {
|
||||
root.showAudioInputDialog = true;
|
||||
}
|
||||
function onOpenBluetoothDialog() {
|
||||
root.showBluetoothDialog = true;
|
||||
}
|
||||
function onOpenNightLightDialog() {
|
||||
root.showNightLightDialog = true;
|
||||
}
|
||||
function onOpenWifiDialog() {
|
||||
root.showWifiDialog = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
component SystemButtonRow: RowLayout {
|
||||
spacing: 10
|
||||
component SystemButtonRow: Item {
|
||||
implicitHeight: Math.max(uptimeContainer.implicitHeight, systemButtonsRow.implicitHeight)
|
||||
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
|
||||
StyledText {
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
Rectangle {
|
||||
id: uptimeContainer
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
left: parent.left
|
||||
}
|
||||
color: Appearance.colors.colLayer1
|
||||
radius: height / 2
|
||||
implicitWidth: uptimeRow.implicitWidth + 24
|
||||
implicitHeight: uptimeRow.implicitHeight + 8
|
||||
|
||||
Row {
|
||||
id: uptimeRow
|
||||
anchors.centerIn: parent
|
||||
spacing: 8
|
||||
CustomIcon {
|
||||
id: distroIcon
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
width: 25
|
||||
height: 25
|
||||
source: SystemInfo.distroIcon
|
||||
colorize: true
|
||||
color: Appearance.colors.colOnLayer0
|
||||
}
|
||||
StyledText {
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
font.pixelSize: Appearance.font.pixelSize.normal
|
||||
color: Appearance.colors.colOnLayer0
|
||||
text: Translation.tr("Up %1").arg(DateTime.uptime)
|
||||
textFormat: Text.MarkdownText
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ButtonGroup {
|
||||
id: systemButtonsRow
|
||||
anchors {
|
||||
top: parent.top
|
||||
bottom: parent.bottom
|
||||
right: parent.right
|
||||
}
|
||||
color: Appearance.colors.colLayer1
|
||||
padding: 4
|
||||
|
||||
QuickToggleButton {
|
||||
toggled: root.editMode
|
||||
visible: Config.options.sidebar.quickToggles.style === "android"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import "./calendar_layout.js" as CalendarLayout
|
||||
import "calendar_layout.js" as CalendarLayout
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import qs
|
||||
import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Io
|
||||
import Quickshell
|
||||
import Quickshell.Wayland
|
||||
import Quickshell.Hyprland
|
||||
|
||||
WindowDialog {
|
||||
id: root
|
||||
property var screen: root.QsWindow.window?.screen
|
||||
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
|
||||
|
||||
WindowDialogTitle {
|
||||
text: Translation.tr("Eye protection")
|
||||
}
|
||||
|
||||
WindowDialogSectionHeader {
|
||||
text: Translation.tr("Night Light")
|
||||
}
|
||||
|
||||
WindowDialogSeparator {
|
||||
Layout.topMargin: -22
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
|
||||
Column {
|
||||
id: nightLightColumn
|
||||
Layout.topMargin: -16
|
||||
Layout.fillWidth: true
|
||||
|
||||
ConfigSwitch {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
buttonIcon: "lightbulb"
|
||||
text: Translation.tr("Enable now")
|
||||
checked: Hyprsunset.active
|
||||
onCheckedChanged: {
|
||||
Hyprsunset.toggle(checked)
|
||||
}
|
||||
}
|
||||
|
||||
ConfigSwitch {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
buttonIcon: "night_sight_auto"
|
||||
text: Translation.tr("Automatic")
|
||||
checked: Config.options.light.night.automatic
|
||||
onCheckedChanged: {
|
||||
Config.options.light.night.automatic = checked;
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogSlider {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: 4
|
||||
rightMargin: 4
|
||||
}
|
||||
text: Translation.tr("Color temperature")
|
||||
from: 1000
|
||||
to: 20000
|
||||
stopIndicatorValues: [6000, to]
|
||||
value: Config.options.light.night.colorTemperature
|
||||
onMoved: Config.options.light.night.colorTemperature = value
|
||||
tooltipContent: `${Math.round(value)}K`
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogSectionHeader {
|
||||
text: Translation.tr("Anti-flashbang (experimental)")
|
||||
}
|
||||
|
||||
WindowDialogSeparator {
|
||||
Layout.topMargin: -22
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
|
||||
Column {
|
||||
id: antiFlashbangColumn
|
||||
Layout.topMargin: -16
|
||||
Layout.fillWidth: true
|
||||
|
||||
ConfigSwitch {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
}
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
buttonIcon: "destruction"
|
||||
text: Translation.tr("Enable")
|
||||
checked: Config.options.light.antiFlashbang.enable
|
||||
onCheckedChanged: {
|
||||
Config.options.light.antiFlashbang.enable = checked;
|
||||
}
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Example use case: eroge on one workspace, dark Discord window on another")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogSectionHeader {
|
||||
text: Translation.tr("Brightness")
|
||||
}
|
||||
|
||||
WindowDialogSeparator {
|
||||
Layout.topMargin: -22
|
||||
Layout.leftMargin: 0
|
||||
Layout.rightMargin: 0
|
||||
}
|
||||
|
||||
Column {
|
||||
id: brightnessColumn
|
||||
Layout.topMargin: -16
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
|
||||
WindowDialogSlider {
|
||||
anchors {
|
||||
left: parent.left
|
||||
right: parent.right
|
||||
leftMargin: 4
|
||||
rightMargin: 4
|
||||
}
|
||||
// text: Translation.tr("Brightness")
|
||||
value: root.brightnessMonitor.brightness
|
||||
onMoved: root.brightnessMonitor.setBrightness(value)
|
||||
}
|
||||
}
|
||||
|
||||
WindowDialogButtonRow {
|
||||
Layout.fillWidth: true
|
||||
|
||||
Item {
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
DialogButton {
|
||||
buttonText: Translation.tr("Done")
|
||||
onClicked: root.dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,9 @@ Rectangle {
|
||||
radius: Appearance.rounding.normal
|
||||
color: Appearance.colors.colLayer1
|
||||
|
||||
signal openWifiDialog()
|
||||
signal openBluetoothDialog()
|
||||
signal openAudioOutputDialog()
|
||||
signal openAudioInputDialog()
|
||||
signal openBluetoothDialog()
|
||||
signal openNightLightDialog()
|
||||
signal openWifiDialog()
|
||||
}
|
||||
|
||||
+27
-22
@@ -6,30 +6,20 @@ import QtQuick.Layouts
|
||||
import Quickshell
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
import "./androidStyle/"
|
||||
import qs.modules.sidebarRight.quickToggles.androidStyle
|
||||
|
||||
AbstractQuickPanel {
|
||||
id: root
|
||||
property bool editMode: false
|
||||
Layout.fillWidth: true
|
||||
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
|
||||
|
||||
// Sizes
|
||||
implicitHeight: (editMode ? contentItem.implicitHeight : usedRows.implicitHeight) + root.padding * 2
|
||||
Behavior on implicitHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
property real spacing: 6
|
||||
property real padding: 6
|
||||
|
||||
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
|
||||
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
|
||||
readonly property list<var> toggles: Config.options.sidebar.quickToggles.android.toggles
|
||||
readonly property list<var> toggleRows: toggleRowsForList(toggles)
|
||||
readonly property list<var> unusedToggles: {
|
||||
const types = availableToggleTypes.filter(type => !toggles.some(toggle => (toggle && toggle.type === type)))
|
||||
return types.map(type => { return { type: type, size: 1 } })
|
||||
}
|
||||
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
|
||||
readonly property real baseCellWidth: {
|
||||
// This is the wrong calculation, but it looks correct in reality???
|
||||
// (theoretically spacing should be multiplied by 1 column less)
|
||||
@@ -38,6 +28,17 @@ AbstractQuickPanel {
|
||||
}
|
||||
readonly property real baseCellHeight: 56
|
||||
|
||||
// Toggles
|
||||
readonly property list<string> availableToggleTypes: ["network", "bluetooth", "idleInhibitor", "easyEffects", "nightLight", "darkMode", "cloudflareWarp", "gameMode", "screenSnip", "colorPicker", "onScreenKeyboard", "mic", "audio", "notifications", "powerProfile"]
|
||||
readonly property int columns: Config.options.sidebar.quickToggles.android.columns
|
||||
readonly property list<var> toggles: Config.ready ? Config.options.sidebar.quickToggles.android.toggles : []
|
||||
readonly property list<var> toggleRows: toggleRowsForList(toggles)
|
||||
readonly property list<var> unusedToggles: {
|
||||
const types = availableToggleTypes.filter(type => !toggles.some(toggle => (toggle && toggle.type === type)))
|
||||
return types.map(type => { return { type: type, size: 1 } })
|
||||
}
|
||||
readonly property list<var> unusedToggleRows: toggleRowsForList(unusedToggles)
|
||||
|
||||
function toggleRowsForList(togglesList) {
|
||||
var rows = [];
|
||||
var row = [];
|
||||
@@ -73,14 +74,14 @@ AbstractQuickPanel {
|
||||
Repeater {
|
||||
id: usedRowsRepeater
|
||||
model: ScriptModel {
|
||||
values: root.toggleRows
|
||||
values: Array(root.toggleRows.length)
|
||||
}
|
||||
delegate: ButtonGroup {
|
||||
id: toggleRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
property var modelData: root.toggleRows[index]
|
||||
property int startingIndex: {
|
||||
const rows = usedRowsRepeater.model.values;
|
||||
const rows = root.toggleRows;
|
||||
let sum = 0;
|
||||
for (let i = 0; i < index; i++) {
|
||||
sum += rows[i].length;
|
||||
@@ -91,7 +92,8 @@ AbstractQuickPanel {
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: toggleRow.modelData
|
||||
values: toggleRow?.modelData ?? []
|
||||
objectProp: "type"
|
||||
}
|
||||
delegate: AndroidToggleDelegateChooser {
|
||||
startingIndex: toggleRow.startingIndex
|
||||
@@ -99,10 +101,11 @@ AbstractQuickPanel {
|
||||
baseCellWidth: root.baseCellWidth
|
||||
baseCellHeight: root.baseCellHeight
|
||||
spacing: root.spacing
|
||||
onOpenWifiDialog: root.openWifiDialog()
|
||||
onOpenBluetoothDialog: root.openBluetoothDialog()
|
||||
onOpenAudioOutputDialog: root.openAudioOutputDialog()
|
||||
onOpenAudioInputDialog: root.openAudioInputDialog()
|
||||
onOpenBluetoothDialog: root.openBluetoothDialog()
|
||||
onOpenNightLightDialog: root.openNightLightDialog()
|
||||
onOpenWifiDialog: root.openWifiDialog()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -131,16 +134,18 @@ AbstractQuickPanel {
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: root.unusedToggleRows
|
||||
values: Array(root.unusedToggleRows.length)
|
||||
}
|
||||
delegate: ButtonGroup {
|
||||
id: unusedToggleRow
|
||||
required property var modelData
|
||||
required property int index
|
||||
property var modelData: root.unusedToggleRows[index]
|
||||
spacing: root.spacing
|
||||
|
||||
Repeater {
|
||||
model: ScriptModel {
|
||||
values: unusedToggleRow.modelData
|
||||
values: unusedToggleRow?.modelData ?? []
|
||||
objectProp: "type"
|
||||
}
|
||||
delegate: AndroidToggleDelegateChooser {
|
||||
startingIndex: -1
|
||||
|
||||
@@ -5,7 +5,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Bluetooth
|
||||
|
||||
import "./classicStyle/"
|
||||
import qs.modules.sidebarRight.quickToggles.classicStyle
|
||||
|
||||
AbstractQuickPanel {
|
||||
id: root
|
||||
|
||||
+2
-2
@@ -19,7 +19,7 @@ AndroidQuickToggleButton {
|
||||
}
|
||||
|
||||
altAction: () => {
|
||||
Config.options.light.night.automatic = !Config.options.light.night.automatic
|
||||
root.openMenu()
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
@@ -27,7 +27,7 @@ AndroidQuickToggleButton {
|
||||
}
|
||||
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Night Light | Right-click to toggle Auto mode")
|
||||
text: Translation.tr("Night Light | Right-click to configure")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+16
-1
@@ -23,6 +23,21 @@ GroupButton {
|
||||
baseHeight: root.baseCellHeight
|
||||
|
||||
property bool editMode: false
|
||||
enableImplicitWidthAnimation: !editMode && root.mouseArea.containsMouse
|
||||
enableImplicitHeightAnimation: !editMode && root.mouseArea.containsMouse
|
||||
Behavior on baseWidth {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
Behavior on baseHeight {
|
||||
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
|
||||
}
|
||||
opacity: 0
|
||||
Component.onCompleted: {
|
||||
opacity = 1
|
||||
}
|
||||
Behavior on opacity {
|
||||
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
|
||||
}
|
||||
|
||||
signal openMenu()
|
||||
|
||||
@@ -65,7 +80,7 @@ GroupButton {
|
||||
MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
fill: root.toggled ? 1 : 0
|
||||
iconSize: Appearance.font.pixelSize.huge
|
||||
iconSize: root.expandedSize ? 22 : 24
|
||||
color: root.colIcon
|
||||
text: root.buttonIcon
|
||||
}
|
||||
|
||||
+2
-1
@@ -4,6 +4,7 @@ import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import Quickshell.Hyprland
|
||||
|
||||
AndroidQuickToggleButton {
|
||||
id: root
|
||||
@@ -22,7 +23,7 @@ AndroidQuickToggleButton {
|
||||
interval: 300
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
|
||||
Hyprland.dispatch("global quickshell:regionScreenshot")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+6
-2
@@ -14,10 +14,11 @@ DelegateChooser {
|
||||
required property real baseCellHeight
|
||||
required property real spacing
|
||||
required property int startingIndex
|
||||
signal openWifiDialog()
|
||||
signal openBluetoothDialog()
|
||||
signal openAudioOutputDialog()
|
||||
signal openAudioInputDialog()
|
||||
signal openBluetoothDialog()
|
||||
signal openNightLightDialog()
|
||||
signal openWifiDialog()
|
||||
|
||||
role: "type"
|
||||
|
||||
@@ -90,6 +91,9 @@ DelegateChooser {
|
||||
baseCellHeight: root.baseCellHeight
|
||||
cellSpacing: root.spacing
|
||||
cellSize: modelData.size
|
||||
onOpenMenu: {
|
||||
root.openNightLightDialog()
|
||||
}
|
||||
} }
|
||||
|
||||
DelegateChoice { roleValue: "darkMode"; AndroidDarkModeToggle {
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "../"
|
||||
import qs.modules.sidebarRight.quickToggles
|
||||
import qs
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
|
||||
+1
-1
@@ -14,7 +14,7 @@ GroupButton {
|
||||
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
iconSize: 20
|
||||
iconSize: 22
|
||||
fill: toggled ? 1 : 0
|
||||
color: toggled ? Appearance.m3colors.m3onPrimary : Appearance.colors.colOnLayer1
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
|
||||
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "./../bar" as Bar
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
@@ -2,7 +2,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../bar" as Bar
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
@@ -8,7 +8,7 @@ import qs.services
|
||||
import qs.modules.common
|
||||
import qs.modules.common.widgets
|
||||
import qs.modules.common.functions
|
||||
import "../bar" as Bar
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
Item { // Bar content region
|
||||
id: root
|
||||
|
||||
@@ -3,7 +3,7 @@ import qs.modules.common.widgets
|
||||
import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import "../bar" as Bar
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
Item {
|
||||
id: root
|
||||
|
||||
@@ -4,7 +4,7 @@ import qs.services
|
||||
import QtQuick
|
||||
import QtQuick.Shapes
|
||||
import QtQuick.Layouts
|
||||
import "../bar" as Bar
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
Item { // Full hitbox
|
||||
id: root
|
||||
|
||||
@@ -8,7 +8,7 @@ import QtQuick
|
||||
import QtQuick.Layouts
|
||||
import Quickshell.Services.Mpris
|
||||
|
||||
import "../bar" as Bar
|
||||
import qs.modules.bar as Bar
|
||||
|
||||
MouseArea {
|
||||
id: root
|
||||
|
||||
@@ -316,7 +316,7 @@ MouseArea {
|
||||
bottomMargin: 8
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
IconToolbarButton {
|
||||
implicitWidth: height
|
||||
onClicked: {
|
||||
Wallpapers.openFallbackPicker(root.useDarkMode);
|
||||
@@ -327,42 +327,27 @@ MouseArea {
|
||||
GlobalStates.wallpaperSelectorOpen = false;
|
||||
Config.options.wallpaperSelector.useSystemFileDialog = true
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "open_in_new"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
text: "open_in_new"
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Use the system file picker instead\nRight-click to make this the default behavior")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
IconToolbarButton {
|
||||
implicitWidth: height
|
||||
onClicked: {
|
||||
Wallpapers.randomFromCurrentFolder();
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "ifl"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
text: "ifl"
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Pick random from this folder")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
IconToolbarButton {
|
||||
implicitWidth: height
|
||||
onClicked: root.useDarkMode = !root.useDarkMode
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: root.useDarkMode ? "dark_mode" : "light_mode"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
text: root.useDarkMode ? "dark_mode" : "light_mode"
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Click to toggle light/dark mode\n(applied when wallpaper is chosen)")
|
||||
}
|
||||
@@ -403,17 +388,12 @@ MouseArea {
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarButton {
|
||||
IconToolbarButton {
|
||||
implicitWidth: height
|
||||
onClicked: {
|
||||
GlobalStates.wallpaperSelectorOpen = false;
|
||||
}
|
||||
contentItem: MaterialSymbol {
|
||||
anchors.centerIn: parent
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
text: "cancel_presentation"
|
||||
iconSize: Appearance.font.pixelSize.larger
|
||||
}
|
||||
text: "close"
|
||||
StyledToolTip {
|
||||
text: Translation.tr("Cancel wallpaper selection")
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@ import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Wayland
|
||||
import QtQuick
|
||||
import "./ai/"
|
||||
import qs.services.ai
|
||||
|
||||
/**
|
||||
* Basic service to handle LLM chats. Supports Google's and OpenAI's API formats.
|
||||
|
||||
@@ -4,6 +4,8 @@ pragma ComponentBehavior: Bound
|
||||
// From https://github.com/caelestia-dots/shell with modifications.
|
||||
// License: GPLv3
|
||||
|
||||
import qs.modules.common
|
||||
import qs.modules.common.functions
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
@@ -14,6 +16,7 @@ import QtQuick
|
||||
*/
|
||||
Singleton {
|
||||
id: root
|
||||
property real minimumBrightnessAllowed: 0.00001 // Setting to 0 would kind of turn off the screen. We don't want that.
|
||||
|
||||
signal brightnessChanged()
|
||||
|
||||
@@ -84,6 +87,8 @@ Singleton {
|
||||
}
|
||||
property int rawMaxBrightness: 100
|
||||
property real brightness
|
||||
property real brightnessMultiplier: 1.0
|
||||
property real multipliedBrightness: Math.max(0, Math.min(1, brightness * brightnessMultiplier))
|
||||
property bool ready: false
|
||||
|
||||
onBrightnessChanged: {
|
||||
@@ -119,17 +124,23 @@ Singleton {
|
||||
}
|
||||
|
||||
function syncBrightness() {
|
||||
const rounded = Math.round(monitor.brightness * monitor.rawMaxBrightness);
|
||||
const brightnessValue = monitor.multipliedBrightness
|
||||
const rounded = Math.round(brightnessValue * monitor.rawMaxBrightness);
|
||||
setProc.command = isDdc ? ["ddcutil", "-b", busNum, "setvcp", "10", rounded] : ["brightnessctl", "--class", "backlight", "s", rounded, "--quiet"];
|
||||
setProc.startDetached();
|
||||
}
|
||||
|
||||
function setBrightness(value: real): void {
|
||||
value = Math.max(0.01, Math.min(1, value));
|
||||
value = Math.max(root.minimumBrightnessAllowed, Math.min(1, value));
|
||||
monitor.brightness = value;
|
||||
setTimer.restart();
|
||||
}
|
||||
|
||||
function setBrightnessMultiplier(value: real): void {
|
||||
monitor.brightnessMultiplier = value;
|
||||
setTimer.restart();
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
initialize();
|
||||
}
|
||||
@@ -145,6 +156,61 @@ Singleton {
|
||||
BrightnessMonitor {}
|
||||
}
|
||||
|
||||
// Anti-flashbang
|
||||
property string screenshotDir: "/tmp/quickshell/brightness/antiflashbang"
|
||||
function brightnessMultiplierForLightness(x: real): real {
|
||||
// 6.600135 + 216.360356 * e^(-0.0811129189x)
|
||||
// Division by 100 is to normalize to [0, 1]
|
||||
return (6.600135 + 216.360356 * Math.pow(Math.E, -0.0811129189 * x)) / 100.0;
|
||||
}
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
Scope {
|
||||
id: screenScope
|
||||
required property var modelData
|
||||
property string screenName: modelData.name
|
||||
property string screenshotPath: `${root.screenshotDir}/screenshot-${screenName}.png`
|
||||
Connections {
|
||||
enabled: Config.options.light.antiFlashbang.enable
|
||||
target: Hyprland
|
||||
function onRawEvent(event) {
|
||||
if (["workspacev2"].includes(event.name)) {
|
||||
screenshotTimer.restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Timer {
|
||||
id: screenshotTimer
|
||||
interval: 700 // This is what I have for a Hyprland ws anim
|
||||
onTriggered: {
|
||||
screenshotProc.running = false;
|
||||
screenshotProc.running = true;
|
||||
}
|
||||
}
|
||||
|
||||
Process {
|
||||
id: screenshotProc
|
||||
command: ["bash", "-c",
|
||||
`mkdir -p '${StringUtils.shellSingleQuoteEscape(root.screenshotDir)}'`
|
||||
+ ` && grim -o '${StringUtils.shellSingleQuoteEscape(screenScope.screenName)}' '${StringUtils.shellSingleQuoteEscape(screenScope.screenshotPath)}'`
|
||||
+ ` && magick '${StringUtils.shellSingleQuoteEscape(screenScope.screenshotPath)}' -colorspace Gray -format "%[fx:mean*100]" info:`
|
||||
]
|
||||
stdout: StdioCollector {
|
||||
id: lightnessCollector
|
||||
onStreamFinished: {
|
||||
Quickshell.execDetached(["rm", screenScope.screenshotPath]); // Cleanup
|
||||
const lightness = lightnessCollector.text
|
||||
const newMultiplier = root.brightnessMultiplierForLightness(parseFloat(lightness))
|
||||
Brightness.getMonitorForScreen(screenScope.modelData).setBrightnessMultiplier(newMultiplier)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// External trigger points
|
||||
|
||||
IpcHandler {
|
||||
target: "brightness"
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import QtQuick
|
||||
import qs.modules.common
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import Quickshell.Hyprland
|
||||
|
||||
/**
|
||||
* Simple hyprsunset service with automatic mode.
|
||||
@@ -111,18 +112,28 @@ Singleton {
|
||||
}
|
||||
}
|
||||
|
||||
function toggle() {
|
||||
function toggle(active = undefined) {
|
||||
if (root.manualActive === undefined) {
|
||||
root.manualActive = root.active;
|
||||
root.manualActiveHour = root.clockHour;
|
||||
root.manualActiveMinute = root.clockMinute;
|
||||
}
|
||||
|
||||
root.manualActive = !root.manualActive;
|
||||
root.manualActive = active !== undefined ? active : !root.manualActive;
|
||||
if (root.manualActive) {
|
||||
root.enable();
|
||||
} else {
|
||||
root.disable();
|
||||
}
|
||||
}
|
||||
|
||||
// Change temp
|
||||
Connections {
|
||||
target: Config.options.light.night
|
||||
function onColorTemperatureChanged() {
|
||||
if (!root.active) return;
|
||||
Hyprland.dispatch(`hyprctl hyprsunset temperature ${Config.options.light.night.colorTemperature}`);
|
||||
Quickshell.execDetached(["hyprctl", "hyprsunset", "temperature", `${Config.options.light.night.colorTemperature}`]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@ pragma ComponentBehavior: Bound
|
||||
import Quickshell
|
||||
import Quickshell.Io
|
||||
import QtQuick
|
||||
import "./network"
|
||||
import qs.services.network
|
||||
|
||||
/**
|
||||
* Network service with nmcli.
|
||||
|
||||
@@ -70,6 +70,8 @@ Singleton {
|
||||
case "debian":
|
||||
case "raspbian":
|
||||
case "kali": distroIcon = "debian-symbolic"; break;
|
||||
case "funtoo":
|
||||
case "gentoo": distroIcon = "gentoo-symbolic"; break;
|
||||
default: distroIcon = "linux-symbolic"; break;
|
||||
}
|
||||
if (textOsRelease.toLowerCase().includes("nyarch")) {
|
||||
|
||||
@@ -79,7 +79,7 @@ Singleton {
|
||||
// Special cases
|
||||
if (!text) return "";
|
||||
var key = text.toString();
|
||||
if (root.isLoading || (!root.translations.hasOwnProperty(key) && !root.generatedTranslations.hasOwnProperty(key)))
|
||||
if (root.isLoading || (!root?.translations?.hasOwnProperty(key) && !root?.generatedTranslations?.hasOwnProperty(key)))
|
||||
return key;
|
||||
|
||||
// Normal cases
|
||||
|
||||
@@ -7,30 +7,30 @@
|
||||
//@ pragma Env QT_SCALE_FACTOR=1
|
||||
|
||||
|
||||
import "./modules/common/"
|
||||
import "./modules/background/"
|
||||
import "./modules/bar/"
|
||||
import "./modules/cheatsheet/"
|
||||
import "./modules/crosshair/"
|
||||
import "./modules/dock/"
|
||||
import "./modules/lock/"
|
||||
import "./modules/mediaControls/"
|
||||
import "./modules/notificationPopup/"
|
||||
import "./modules/onScreenDisplay/"
|
||||
import "./modules/onScreenKeyboard/"
|
||||
import "./modules/overview/"
|
||||
import "./modules/regionSelector/"
|
||||
import "./modules/screenCorners/"
|
||||
import "./modules/sessionScreen/"
|
||||
import "./modules/sidebarLeft/"
|
||||
import "./modules/sidebarRight/"
|
||||
import "./modules/verticalBar/"
|
||||
import "./modules/wallpaperSelector/"
|
||||
import qs.modules.common
|
||||
import qs.modules.background
|
||||
import qs.modules.bar
|
||||
import qs.modules.cheatsheet
|
||||
import qs.modules.crosshair
|
||||
import qs.modules.dock
|
||||
import qs.modules.lock
|
||||
import qs.modules.mediaControls
|
||||
import qs.modules.notificationPopup
|
||||
import qs.modules.onScreenDisplay
|
||||
import qs.modules.onScreenKeyboard
|
||||
import qs.modules.overview
|
||||
import qs.modules.regionSelector
|
||||
import qs.modules.screenCorners
|
||||
import qs.modules.sessionScreen
|
||||
import qs.modules.sidebarLeft
|
||||
import qs.modules.sidebarRight
|
||||
import qs.modules.verticalBar
|
||||
import qs.modules.wallpaperSelector
|
||||
|
||||
import QtQuick
|
||||
import QtQuick.Window
|
||||
import Quickshell
|
||||
import "./services/"
|
||||
import qs.services
|
||||
|
||||
ShellRoot {
|
||||
// Enable/disable modules here. False = not loaded at all, so rest assured
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
# This script depends on `functions.sh' .
|
||||
# This is NOT a script for execution, but for loading functions, so NOT need execution permission or shebang.
|
||||
# NOTE that you NOT need to `cd ..' because the `$0' is NOT this file, but the script file which will source this file.
|
||||
# This script is not for direct execution, instead it should be sourced by other script. It does not need execution permission or shebang.
|
||||
|
||||
# shellcheck shell=bash
|
||||
|
||||
@@ -105,5 +104,5 @@ install-python-packages(){
|
||||
x uv venv --prompt .venv $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV) -p 3.12
|
||||
x source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate
|
||||
x uv pip install -r sdata/uv/requirements.txt
|
||||
x deactivate # We don't need the virtual environment anymore
|
||||
x deactivate
|
||||
}
|
||||
|
||||
@@ -14,11 +14,12 @@ Options for install:
|
||||
--skip-allsetups Skip the whole process setting up permissions/services etc
|
||||
--skip-allfiles Skip the whole process copying configuration files
|
||||
-s, --skip-sysupdate Skip system package upgrade e.g. \"sudo pacman -Syu\"
|
||||
--skip-quickshell Skip installing the config for Quickshell
|
||||
--skip-hyprland Skip installing the config for Hyprland
|
||||
--skip-fish Skip installing the config for Fish
|
||||
--skip-plasmaintg Skip installing plasma-browser-integration
|
||||
--skip-miscconf Skip copying the dirs and files to \".configs\" except for
|
||||
AGS, Fish and Hyprland
|
||||
Quickshell, Fish and Hyprland
|
||||
--exp-files Use experimental script for the third step copying files
|
||||
--fontset <set> (Unavailable yet) Use a set of pre-defined font and config
|
||||
--via-nix (Unavailable yet) Use Nix to install dependencies
|
||||
@@ -32,7 +33,7 @@ cleancache(){
|
||||
# `man getopt` to see more
|
||||
para=$(getopt \
|
||||
-o hfk:cs \
|
||||
-l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \
|
||||
-l help,force,fontset:,clean,skip-allgreeting,skip-alldeps,skip-allsetups,skip-allfiles,skip-sysupdate,skip-quickshell,skip-fish,skip-hyprland,skip-plasmaintg,skip-miscconf,exp-files,via-nix \
|
||||
-n "$0" -- "$@")
|
||||
[ $? != 0 ] && echo "$0: Error when getopt, please recheck parameters." && exit 1
|
||||
#####################################################################################
|
||||
@@ -64,6 +65,7 @@ while true ; do
|
||||
-s|--skip-sysupdate) SKIP_SYSUPDATE=true;shift;;
|
||||
--skip-hyprland) SKIP_HYPRLAND=true;shift;;
|
||||
--skip-fish) SKIP_FISH=true;shift;;
|
||||
--skip-quickshell) SKIP_QUICKSHELL=true;shift;;
|
||||
--skip-miscconf) SKIP_MISCCONF=true;shift;;
|
||||
--skip-plasmaintg) SKIP_PLASMAINTG=true;shift;;
|
||||
--exp-files) EXPERIMENTAL_FILES_SCRIPT=true;shift;;
|
||||
|
||||
@@ -53,21 +53,27 @@ function ask_backup_configs(){
|
||||
showfun backup_clashing_targets
|
||||
printf "${STY_RED}"
|
||||
printf "Would you like to backup clashing dirs/files under \"$XDG_CONFIG_HOME\" and \"$XDG_DATA_HOME\" to \"$BACKUP_DIR\"?"
|
||||
read -p "[y/N] " backup_confirm
|
||||
case $backup_confirm in
|
||||
[yY][eE][sS]|[yY])
|
||||
backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config"
|
||||
backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share"
|
||||
;;
|
||||
*) echo "Skipping backup..." ;;
|
||||
esac
|
||||
printf "${STY_RST}"
|
||||
while true;do
|
||||
echo " y = Yes, backup"
|
||||
echo " n = No, skip to next"
|
||||
local p; read -p "====> " p
|
||||
case $p in
|
||||
[yY]) echo -e "${STY_BLUE}OK, doing backup...${STY_RST}" ;local backup=true;break ;;
|
||||
[nN]) echo -e "${STY_BLUE}Alright, skipping...${STY_RST}" ;local backup=false;break ;;
|
||||
*) echo -e "${STY_RED}Please enter [y/n].${STY_RST}";;
|
||||
esac
|
||||
done
|
||||
if $backup;then
|
||||
backup_clashing_targets dots/.config $XDG_CONFIG_HOME "${BACKUP_DIR}/.config"
|
||||
backup_clashing_targets dots/.local/share $XDG_DATA_HOME "${BACKUP_DIR}/.local/share"
|
||||
fi
|
||||
}
|
||||
|
||||
#####################################################################################
|
||||
|
||||
# In case some dirs does not exists
|
||||
v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME $XDG_DATA_HOME
|
||||
v mkdir -p $XDG_BIN_HOME $XDG_CACHE_HOME $XDG_CONFIG_HOME/quickshell $XDG_DATA_HOME
|
||||
|
||||
case $ask in
|
||||
false) sleep 0 ;;
|
||||
@@ -84,11 +90,11 @@ esac
|
||||
# original dotfiles and new ones in the SAME DIRECTORY
|
||||
# (eg. in ~/.config/hypr) won't be mixed together
|
||||
|
||||
# MISC (For dots/.config/* but not fish, not Hyprland)
|
||||
# MISC (For dots/.config/* but not quickshell, not fish, not Hyprland)
|
||||
case $SKIP_MISCCONF in
|
||||
true) sleep 0;;
|
||||
*)
|
||||
for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do
|
||||
for i in $(find dots/.config/ -mindepth 1 -maxdepth 1 ! -name 'quickshell' ! -name 'fish' ! -name 'hypr' -exec basename {} \;); do
|
||||
# i="dots/.config/$i"
|
||||
echo "[$0]: Found target: dots/.config/$i"
|
||||
if [ -d "dots/.config/$i" ];then warning_rsync; v rsync -av --delete "dots/.config/$i/" "$XDG_CONFIG_HOME/$i/"
|
||||
@@ -98,6 +104,13 @@ case $SKIP_MISCCONF in
|
||||
;;
|
||||
esac
|
||||
|
||||
case $SKIP_QUICKSHELL in
|
||||
true) sleep 0;;
|
||||
*)
|
||||
warning_rsync; v rsync -av --delete dots/.config/quickshell/ii/ "$XDG_CONFIG_HOME"/quickshell/ii/
|
||||
;;
|
||||
esac
|
||||
|
||||
case $SKIP_FISH in
|
||||
true) sleep 0;;
|
||||
*)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
_commit='00858812f25b748d08b075a0d284093685fa3ffd'
|
||||
_commit='3e2ce40b18af943f9ba370ed73565e9f487663ef'
|
||||
# Useful links:
|
||||
# https://git.outfoxxed.me/quickshell/quickshell/commits/branch/master
|
||||
# https://aur.archlinux.org/packages/quickshell-git
|
||||
|
||||
+15
-13
@@ -3,21 +3,17 @@
|
||||
|
||||
EAPI=8
|
||||
|
||||
inherit cmake
|
||||
inherit cmake git-r3
|
||||
|
||||
DESCRIPTION="Toolkit for building desktop widgets using QtQuick"
|
||||
HOMEPAGE="https://quickshell.org/"
|
||||
|
||||
if [[ "${PV}" = *9999 ]]; then
|
||||
inherit git-r3
|
||||
EGIT_REPO_URI="https://github.com/quickshell-mirror/${PN^}.git"
|
||||
else
|
||||
SRC_URI="https://github.com/quickshell-mirror/${PN}/archive/refs/tags/v${PV}.tar.gz -> ${P}.tar.gz"
|
||||
fi
|
||||
EGIT_REPO_URI="https://github.com/quickshell-mirror/quickshell.git"
|
||||
EGIT_COMMIT="3e2ce40b18af943f9ba370ed73565e9f487663ef"
|
||||
|
||||
KEYWORDS="~amd64 ~arm64 ~x86"
|
||||
LICENSE="LGPL-3"
|
||||
SLOT="0"
|
||||
KEYWORDS="~amd64 ~arm64 ~x86"
|
||||
# Upstream recommends leaving all build options enabled by default
|
||||
IUSE="+breakpad +jemalloc +sockets +wayland +layer-shell +session-lock +toplevel-management +screencopy +X +pipewire +tray +mpris +pam +hyprland +hyprland-global-shortcuts +hyprland-focus-grab +i3 +i3-ipc +bluetooth"
|
||||
|
||||
@@ -38,21 +34,27 @@ RDEPEND="
|
||||
mpris? ( dev-qt/qtdbus )
|
||||
pam? ( sys-libs/pam )
|
||||
bluetooth? ( net-wireless/bluez )
|
||||
|
||||
|
||||
|
||||
"
|
||||
DEPEND="${RDEPEND}"
|
||||
BDEPEND="
|
||||
|| ( >=sys-devel/gcc-14:* >=llvm-core/clang-17:* )
|
||||
dev-build/cmake
|
||||
dev-build/ninja
|
||||
virtual/pkgconfig
|
||||
dev-cpp/cli11
|
||||
|
||||
dev-util/spirv-tools
|
||||
dev-qt/qtshadertools:6
|
||||
breakpad? ( dev-util/breakpad )
|
||||
wayland? (
|
||||
dev-util/wayland-scanner
|
||||
dev-libs/wayland-protocols
|
||||
)
|
||||
dev-cpp/cli11
|
||||
dev-build/ninja
|
||||
dev-build/cmake
|
||||
dev-vcs/git
|
||||
virtual/pkgconfig
|
||||
breakpad? ( dev-util/breakpad )
|
||||
|
||||
"
|
||||
|
||||
src_configure(){
|
||||
+5
-2
@@ -15,8 +15,11 @@ DEPEND=""
|
||||
RDEPEND="
|
||||
gui-apps/fuzzel
|
||||
dev-libs/glib
|
||||
gui-apps/quickshell
|
||||
media-gfx/imagemagick
|
||||
gui-apps/hypridle
|
||||
gui-libs/hyprutils
|
||||
gui-apps/hyprlock
|
||||
gui-apps/hyprpicker
|
||||
app-i18n/translate-shell
|
||||
gui-apps/wlogout
|
||||
media-gfx/imagemagick
|
||||
"
|
||||
@@ -37,7 +37,7 @@ fi
|
||||
arch=$(portageq envvar ACCEPT_KEYWORDS)
|
||||
|
||||
# Exclude hyprland, will deal with that separately
|
||||
metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets})
|
||||
metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,quickshell-git,screencapture,toolkit,widgets})
|
||||
|
||||
ebuild_dir="/var/db/repos/localrepo"
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ app-misc/illogical-impulse-microtex-git
|
||||
app-misc/illogical-impulse-oneui4-icons-git
|
||||
app-misc/illogical-impulse-portal
|
||||
app-misc/illogical-impulse-python
|
||||
app-misc/illogical-impulse-quickshell-git
|
||||
app-misc/illogical-impulse-screencapture
|
||||
app-misc/illogical-impulse-toolkit
|
||||
app-misc/illogical-impulse-widgets
|
||||
@@ -39,3 +40,6 @@ gui-libs/hyprland-qt-support **
|
||||
gui-libs/hyprland-qtutils **
|
||||
gui-wm/hyprland **
|
||||
x11-libs/libxkbcommon
|
||||
dev-util/breakpad
|
||||
dev-libs/linux-syscall-support
|
||||
dev-embedded/libdisasm
|
||||
|
||||
@@ -111,7 +111,7 @@ sys-power/upower introspection
|
||||
gui-apps/fuzzel png svg
|
||||
dev-libs/glib dbus elf introspection mime xattr
|
||||
# ngl idk about nm-connection-editor. Works fine without
|
||||
gui-apps/quickshell -X -i3 -i3-ipc -breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland
|
||||
gui-apps/quickshell -X -i3 -i3-ipc breakpad bluetooth hyprland hyprland-focus-grab hyprland-global-shortcuts jemalloc layer-shell mpris pam pipewire screencopy session-lock sockets toplevel-management tray wayland
|
||||
#app-i18n/translate-shell (nothing needed)
|
||||
#gui-apps/wlogout (no use flags)
|
||||
media-gfx/imagemagick xml
|
||||
|
||||
@@ -18,6 +18,9 @@ Syntax:
|
||||
|
||||
Subcommands:
|
||||
install (Default) Install/Reinstall/Update illogical-impulse.
|
||||
install-deps Run the install step \"1. Install dependencies\"
|
||||
install-setups Run the install step \"2. Setup for permissions/services etc\"
|
||||
install-files Run the install step \"3. Copying config files\"
|
||||
exp-uninstall (Experimental) Uninstall illogical-impulse.
|
||||
exp-update (Experimental) Update illogical-impulse without fully reinstall.
|
||||
help Show this help message.
|
||||
@@ -30,7 +33,8 @@ case $1 in
|
||||
# Global help
|
||||
help|--help|-h)showhelp_global;exit;;
|
||||
# Correct subcommand
|
||||
install|exp-uninstall|exp-update)SCRIPT_SUBCOMMAND=$1;shift;;
|
||||
install|install-deps|install-setups|install-files|exp-uninstall|exp-update)
|
||||
SCRIPT_SUBCOMMAND=$1;shift;;
|
||||
# No subcommand
|
||||
-*|"")SCRIPT_SUBCOMMAND=install;;
|
||||
# Wrong subcommand
|
||||
@@ -60,6 +64,31 @@ case ${SCRIPT_SUBCOMMAND} in
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
install-deps)
|
||||
source ./sdata/options/install.sh
|
||||
if [[ "${SKIP_ALLDEPS}" != true ]]; then
|
||||
printf "${STY_CYAN}[$0]: 1. Install dependencies\n${STY_RST}"
|
||||
source ./sdata/step/1.install-deps-selector.sh
|
||||
fi
|
||||
;;
|
||||
install-setups)
|
||||
source ./sdata/options/install.sh
|
||||
if [[ "${SKIP_ALLSETUPS}" != true ]]; then
|
||||
printf "${STY_CYAN}[$0]: 2. Setup for permissions/services etc\n${STY_RST}"
|
||||
source ./sdata/step/2.install-setups-selector.sh
|
||||
fi
|
||||
;;
|
||||
install-files)
|
||||
source ./sdata/options/install.sh
|
||||
if [[ "${SKIP_ALLFILES}" != true ]]; then
|
||||
printf "${STY_CYAN}[$0]: 3. Copying config files\n${STY_RST}"
|
||||
if [[ "${EXPERIMENTAL_FILES_SCRIPT}" == true ]]; then
|
||||
source ./sdata/step/3.install-files.experimental.sh
|
||||
else
|
||||
source ./sdata/step/3.install-files.sh
|
||||
fi
|
||||
fi
|
||||
;;
|
||||
exp-uninstall)
|
||||
source ./sdata/options/exp-uninstall.sh
|
||||
source ./sdata/step/exp-uninstall.sh
|
||||
|
||||
Reference in New Issue
Block a user