diff --git a/dots/.config/quickshell/ii/GlobalStates.qml b/dots/.config/quickshell/ii/GlobalStates.qml index ba680220b..bfb531e6e 100644 --- a/dots/.config/quickshell/ii/GlobalStates.qml +++ b/dots/.config/quickshell/ii/GlobalStates.qml @@ -24,6 +24,7 @@ Singleton { property bool screenLocked: false property bool screenLockContainsCharacters: false property bool screenUnlockFailed: false + property bool screenTranslatorOpen: false property bool sessionOpen: false property bool superDown: false property bool superReleaseMightTrigger: true diff --git a/dots/.config/quickshell/ii/modules/common/Config.qml b/dots/.config/quickshell/ii/modules/common/Config.qml index a0fed3a99..c26fbaec3 100644 --- a/dots/.config/quickshell/ii/modules/common/Config.qml +++ b/dots/.config/quickshell/ii/modules/common/Config.qml @@ -224,7 +224,7 @@ Singleton { property bool vertical: false property bool autoVertical: false property bool enableWorkspace: true - property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size + property real workspaceZoom: 1.0 // Relative to wallpaper size property bool enableSidebar: true property real widgetsFactor: 1.2 } diff --git a/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudTranslate.qml b/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudTranslate.qml new file mode 100644 index 000000000..d117e8d3a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudTranslate.qml @@ -0,0 +1,69 @@ +pragma ComponentBehavior: Bound +import QtQuick +import qs.modules.common.functions +import qs.modules.common.utils +import qs.services +import ".." + +NestableObject { + id: root + + enum State { + Done, Preparing, Processing + } + + signal finished() + property var outputData + property var state: GCloudTranslate.State.Done + + property list pendingStrings + property bool setupReady: false + readonly property bool preparationReady: GoogleCloud.tokenReady && setupReady + + function translateStrings(strings: list) { + GoogleCloud.load(); + root.setupReady = false; + root.pendingStrings = strings; + root.state = GCloudTranslate.State.Preparing; + root.setupReady = true; + } + + onPreparationReadyChanged: { + if (!preparationReady) return; + root.state = GCloudTranslate.State.Processing; + + const targetLang = Translation.languageCode; + const payload = { + "targetLanguageCode": targetLang, + "contents": root.pendingStrings, + "mimeType": "text/plain" + }; + + // print("PENDING STRINGS:", root.pendingStrings) + + var seq = []; + seq.push([ // + "bash", "-c", // + `curl -sL -X POST \ +-H "Authorization: Bearer ${GoogleCloud.token}" \ +-H "x-goog-user-project: ${GoogleCloud.projectId}" \ +-H "Content-Type: application/json" \ +-d '${StringUtils.shellSingleQuoteEscape(JSON.stringify(payload))}' \ +"https://translation.googleapis.com/v3/projects/${GoogleCloud.projectId}:translateText"` + ]); + + seq.push(((out) => { + // print(out) + root.outputData = JSON.parse(out); + root.pendingStrings = []; + root.finished(); + root.state = GCloudTranslate.State.Done; + })); + + multiproc.runSequence(seq); + } + + MultiTurnProcess { + id: multiproc + } +} diff --git a/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudVision.qml b/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudVision.qml new file mode 100644 index 000000000..3e02fd561 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudVision.qml @@ -0,0 +1,93 @@ +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import qs.modules.common.functions +import qs.modules.common.utils +import qs.services +import qs.modules.common +import ".." + +NestableObject { + id: root + + enum State { + Done, Uploading, Processing, Error + } + + signal finished() + signal error() + property var outputData + property var state: GCloudVision.State.Done + + readonly property string imageBase64FilePath: `${Directories.screenshotTemp}/vision_base64.txt` + readonly property string payloadFilePath: `${Directories.screenshotTemp}/vision_payload.json` + property string uploadEndpoint: "https://uguu.se/upload" + + property bool tokenReady: GoogleCloud.tokenReady + property bool onlineImageReady: false + readonly property bool preparationReady: tokenReady && onlineImageReady + + function annotateImage(imageUri: string) { + root.state = GCloudVision.State.Uploading; + root.onlineImageReady = false + GoogleCloud.load(); + + var seq = []; // command sequence + + const niceFilePath = StringUtils.shellSingleQuoteEscape(FileUtils.trimFileProtocol(imageUri)) + seq = [ // + ["bash", "-c", `mkdir -p '${Directories.screenshotTemp}'; base64 '${niceFilePath}' -w 0 > '${imageBase64FilePath}'`], // + (out) => { // + root.onlineImageReady = true; // + } + ] + + // Execute the base64 conversion & load the token + prepMultiproc.runSequence(seq); + } + + onPreparationReadyChanged: { + if (!preparationReady) return; + if (GoogleCloud.tokenError || GoogleCloud.keyError) { + root.state = GCloudVision.State.Error; + root.error(); + return; + } + root.state = GCloudVision.State.Processing; + var seq = []; // command sequence + + // Construct the JSON payload using jq to read from the base64 file + seq.push([ + "bash", "-c", + `jq -n --rawfile content '${imageBase64FilePath}' \ +'{"requests": [{"image": {"content": $content}, "features": [{"type": "DOCUMENT_TEXT_DETECTION"}]}]}' \ +> '${payloadFilePath}'` + ]); + + seq.push([ + "bash", "-c", + `curl -s -X POST \ +-H "Authorization: Bearer ${GoogleCloud.token}" \ +-H "x-goog-user-project: ${GoogleCloud.projectId}" \ +-H "Content-Type: application/json" \ +https://vision.googleapis.com/v1/images:annotate \ +-d @'${payloadFilePath}'` + ]); + + seq.push((out) => { + root.outputData = JSON.parse(out); + root.finished(); + root.state = GCloudVision.State.Done; + }); + + lookMultiproc.runSequence(seq); + } + + MultiTurnProcess { + id: prepMultiproc + } + + MultiTurnProcess { + id: lookMultiproc + } +} diff --git a/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudVisionResult.qml b/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudVisionResult.qml new file mode 100644 index 000000000..d0794b37a --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/models/gCloud/GCloudVisionResult.qml @@ -0,0 +1,90 @@ +pragma ComponentBehavior: Bound +import QtQuick +import ".." + +NestableObject { + id: root + + property real confidenceThreshold: 0.5 // TODO tune this + + property var rawData + property var rawBlocks + property var rawParagraphs + property var coherentParagraphs + + function initializeWithData(apiOutputData: var): void { + // Null check + if (!apiOutputData) { + print("[GCloudVisionResult] Data is null/undefined") + return; + } + + // Raw data + root.rawData = apiOutputData + + // Raw blocks + var pages = apiOutputData.responses[0].fullTextAnnotation.pages + var blocks = []; + for (var i = 0; i < pages.length; i++) { + // print("this page", JSON.stringify(pages[i])) + var blocksThisPage = pages[i].blocks; + for (var j = 0; j < blocksThisPage.length; j++) { + const block = blocksThisPage[j]; + // print("new block with confidence", block.confidence, ":", JSON.stringify(block, null, 2)) + if (block.confidence > root.confidenceThreshold) { + blocks.push(block); + } + } + } + + root.rawBlocks = blocks + // print("RAW BLOCKS:", blocks) + + // Raw paragraphs + var paragraphs = [] + for (var i = 0; i < blocks.length; i++) { + var blockParagraphs = blocks[i].paragraphs; + for (var j = 0; j < blockParagraphs.length; j++) { + const para = blockParagraphs[j]; + // print("new paragraph", JSON.stringify(para)) + paragraphs.push(para); + } + } + root.rawParagraphs = [...paragraphs]; + + // print("RAW PARAGRAPHS", paragraphs) + + // Coherent paragraphs + // (raw data can be as granular as symbols) + // We're interested in paragraph level of granularity as it's good for translations + for (var i = 0; i < paragraphs.length; i++) { + const paragraph = paragraphs[i]; + const words = paragraph.words; + var strList = [] + for (var j = 0; j < words.length; j++) { + const symbols = words[j].symbols; + for (var k = 0; k < symbols.length; k++) { + const sym = symbols[k]; + strList.push(sym.text); + // print("CHAR:", JSON.stringify(sym, null, 2)); + // Breaks + // Reference: https://docs.cloud.google.com/vision/docs/reference/rpc/google.cloud.vision.v1#breaktype + if (sym.property?.detectedBreak.type == "SPACE" || sym.property?.detectedBreak.type == "UNKNOWN") { + strList.push(" "); + } else if (sym.property?.detectedBreak.type == "SURE_SPACE") { + strList.push(" "); + } else if (sym.property?.detectedBreak.type == "EOL_SURE_SPACE" || sym.property?.detectedBreak.type == "LINE_BREAK") { + strList.push("\n"); + } else if (sym.property?.detectedBreak.type == "HYPHEN") { + strList.push("-\n"); + } + } + } + // print("STR LIST:", strList) + paragraphs[i].text = strList.join("").trim(); + // print("PARA TEXT:", paragraphs[i].text) + } + root.coherentParagraphs = paragraphs + // print("COHERENT PARAGRAPHS", JSON.stringify(paragraphs)) + } +} diff --git a/dots/.config/quickshell/ii/modules/common/widgets/SqueezedAnnotationStyledText.qml b/dots/.config/quickshell/ii/modules/common/widgets/SqueezedAnnotationStyledText.qml new file mode 100644 index 000000000..25990cd42 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/common/widgets/SqueezedAnnotationStyledText.qml @@ -0,0 +1,74 @@ +pragma ComponentBehavior: Bound +import QtQuick +import qs.modules.common + +// Annotation similar to how Google Lens does it. +Item { + id: root + + property real scaleFactor: 1.0 + property alias font: textWidget.font + property alias color: textWidget.color + property string text: "" + + property bool rotate90: false + property real maxFontPixelSize: 100 + visible: false + + Component.onCompleted: updateText() + onTextChanged: updateText() + + property bool searching: false + property real searchPixelSize: Appearance.font.pixelSize.small + property real renderPixelSize: Appearance.font.pixelSize.small + font.pixelSize: searching ? searchPixelSize : (renderPixelSize * scaleFactor) + + function updateText() { + // Do we rotate? + + root.rotate90 = false; + const textAspectRatio = textMetrics.width / textMetrics.height + const areaAspectRatio = root.width / root.height + if ((textAspectRatio > 1 && areaAspectRatio < 1) || (textAspectRatio < 1 && areaAspectRatio > 1)) { + root.rotate90 = true; + } + const targetWidth = (root.rotate90 ? root.height : root.width) / root.scaleFactor; + const targetHeight = (root.rotate90 ? root.width : root.height) / root.scaleFactor; + + // Binary search to find the correct font size + var lower = 0 + var upper = maxFontPixelSize + root.searching = true; + while (upper - lower > 0.00001) { + var mid = (lower + upper) / 2; + // print("bin searching", mid, "target", targetWidth, targetHeight, "actual", textWidget.contentWidth, textWidget.contentHeight); + root.searchPixelSize = mid + if (textWidget.contentHeight > targetHeight) { + upper = mid + } else { + lower = mid + } + } + root.renderPixelSize = lower + root.searching = false; + root.visible = true + } + + TextMetrics { + id: textMetrics + text: root.text + font: root.font + } + + StyledText { + id: textWidget + + anchors.centerIn: parent + width: root.rotate90 ? parent.height : parent.width + text: root.text + rotation: root.rotate90 ? 90 : 0 + + renderType: Text.QtRendering + wrapMode: Text.Wrap + } +} diff --git a/dots/.config/quickshell/ii/modules/ii/background/Background.qml b/dots/.config/quickshell/ii/modules/ii/background/Background.qml index 68d8b1698..b63ba8862 100644 --- a/dots/.config/quickshell/ii/modules/ii/background/Background.qml +++ b/dots/.config/quickshell/ii/modules/ii/background/Background.qml @@ -37,6 +37,8 @@ Variants { property list relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor?.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id) property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1 property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10 + property int workspaceChunkSize: Config?.options.bar.workspaces.shown ?? 10 + property int totalWorkspaces: Math.ceil(lastWorkspaceId / workspaceChunkSize) * workspaceChunkSize // Wallpaper property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4") || Config.options.background.wallpaperPath.endsWith(".webm") || Config.options.background.wallpaperPath.endsWith(".mkv") || Config.options.background.wallpaperPath.endsWith(".avi") || Config.options.background.wallpaperPath.endsWith(".mov") property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath @@ -46,13 +48,15 @@ Variants { const sensitiveNetwork = (CF.StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords)); return enabled && sensitiveWallpaper && sensitiveNetwork; } - property real wallpaperToScreenRatio: Math.min(wallpaperWidth / screen.width, wallpaperHeight / screen.height) - property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom + readonly property real parallaxRation: 1.1 + readonly property real additionalScaleFactor: Config.options.background.parallax.workspaceZoom property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated - property real movableXSpace: ((wallpaperWidth / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.width) / 2 - property real movableYSpace: ((wallpaperHeight / wallpaperToScreenRatio * effectiveWallpaperScale) - screen.height) / 2 + property real scaledWallpaperWidth: wallpaperWidth * effectiveWallpaperScale + property real scaledWallpaperHeight: wallpaperHeight * effectiveWallpaperScale + property real parallaxTotalPixelsX: Math.max(0, scaledWallpaperWidth - screen.width) + property real parallaxTotalPixelsY: Math.max(0, scaledWallpaperHeight - screen.height) readonly property bool verticalParallax: (Config.options.background.parallax.autoVertical && wallpaperHeight > wallpaperWidth) || Config.options.background.parallax.vertical // Colors property bool shouldBlur: (GlobalStates.screenLocked && Config.options.lock.blur.enable) @@ -111,20 +115,18 @@ Variants { bgRoot.wallpaperWidth = width; bgRoot.wallpaperHeight = height; - if (width <= screenWidth || height <= screenHeight) { - // Undersized/perfectly sized wallpapers - bgRoot.effectiveWallpaperScale = Math.max(screenWidth / width, screenHeight / height); - } else { - // Oversized = can be zoomed for parallax, yay - bgRoot.effectiveWallpaperScale = Math.min(bgRoot.preferredWallpaperScale, width / screenWidth, height / screenHeight); - } + // Perfect image; scale = 1 + // Small picture; scale > 1; will zoom in the picture + // Big picture; scale < 1; will zoom out the picture + // Choose max number so every side will fit + const minSuitableScale = Math.max(screenWidth / width, screenHeight / height); + bgRoot.effectiveWallpaperScale = minSuitableScale * bgRoot.additionalScaleFactor * bgRoot.parallaxRation; } } } Item { anchors.fill: parent - clip: true // Wallpaper StyledImage { @@ -133,32 +135,52 @@ Variants { opacity: (status === Image.Ready && !bgRoot.wallpaperIsVideo) ? 1 : 0 cache: false smooth: false - // Range = groups that workspaces span on - property int chunkSize: Config?.options.bar.workspaces.shown ?? 10 - property int lower: Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize - property int upper: Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize - property int range: upper - lower - property real valueX: { - let result = 0.5; + + property int workspaceIndex: (bgRoot.monitor.activeWorkspace?.id ?? 1) - 1 + property real middleFraction: 0.5 + property real fraction: { + // 0 - start of the picture + // 1 - end of the picture + if (bgRoot.totalWorkspaces <= 1) { + return middleFraction; + } + return Math.max(0, Math.min(1, workspaceIndex / (bgRoot.totalWorkspaces - 1))); + } + + property real usedFractionX: { + let usedFraction = middleFraction; if (Config.options.background.parallax.enableWorkspace && !bgRoot.verticalParallax) { - result = ((bgRoot.monitor.activeWorkspace?.id - lower) / range); + usedFraction = fraction; } if (Config.options.background.parallax.enableSidebar) { - result += (0.15 * GlobalStates.sidebarRightOpen - 0.15 * GlobalStates.sidebarLeftOpen); + let sidebarFraction = bgRoot.parallaxRation / bgRoot.workspaceChunkSize / 2; + usedFraction += (sidebarFraction * GlobalStates.sidebarRightOpen - sidebarFraction * GlobalStates.sidebarLeftOpen); } - return result; + return Math.max(0, Math.min(1, usedFraction)); } - property real valueY: { - let result = 0.5; + property real usedFractionY: { + let usedFraction = middleFraction; if (Config.options.background.parallax.enableWorkspace && bgRoot.verticalParallax) { - result = ((bgRoot.monitor.activeWorkspace?.id - lower) / range); + usedFraction = fraction; } - return result; + return Math.max(0, Math.min(1, usedFraction)); } - property real effectiveValueX: Math.max(0, Math.min(1, valueX)) - property real effectiveValueY: Math.max(0, Math.min(1, valueY)) - x: -(bgRoot.movableXSpace) - (effectiveValueX - 0.5) * 2 * bgRoot.movableXSpace - y: -(bgRoot.movableYSpace) - (effectiveValueY - 0.5) * 2 * bgRoot.movableYSpace + + x: { + if (bgRoot.screen.width > bgRoot.scaledWallpaperWidth) { + // Center the picture + return (bgRoot.screen.width - bgRoot.scaledWallpaperWidth) / 2; + } + return - bgRoot.parallaxTotalPixelsX * usedFractionX; + } + y: { + if (bgRoot.screen.height > bgRoot.scaledWallpaperHeight) { + // Center the picture + return (bgRoot.screen.height - bgRoot.scaledWallpaperHeight) / 2; + } + return - bgRoot.parallaxTotalPixelsY * usedFractionY; + } + source: bgRoot.wallpaperSafetyTriggered ? "" : bgRoot.wallpaperPath fillMode: Image.PreserveAspectCrop Behavior on x { @@ -174,11 +196,11 @@ Variants { } } sourceSize { - width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale * bgRoot.monitor.scale - height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale * bgRoot.monitor.scale + width: bgRoot.scaledWallpaperWidth + height: bgRoot.scaledWallpaperHeight } - width: bgRoot.wallpaperWidth / bgRoot.wallpaperToScreenRatio * bgRoot.effectiveWallpaperScale - height: bgRoot.wallpaperHeight / bgRoot.wallpaperToScreenRatio * bgRoot.effectiveWallpaperScale + width: bgRoot.scaledWallpaperWidth + height: bgRoot.scaledWallpaperHeight } Loader { @@ -209,53 +231,20 @@ Variants { WidgetCanvas { id: widgetCanvas - anchors { - left: wallpaper.left - right: wallpaper.right - top: wallpaper.top - bottom: wallpaper.bottom - horizontalCenter: undefined - verticalCenter: undefined - readonly property real parallaxFactor: Config.options.background.parallax.widgetsFactor - leftMargin: { - const xOnWallpaper = bgRoot.movableXSpace; - const extraMove = (wallpaper.effectiveValueX * 2 * bgRoot.movableXSpace) * (parallaxFactor - 1); - return xOnWallpaper - extraMove; - } - topMargin: { - const yOnWallpaper = bgRoot.movableYSpace; - const extraMove = (wallpaper.effectiveValueY * 2 * bgRoot.movableYSpace) * (parallaxFactor - 1); - return yOnWallpaper - extraMove; - } - Behavior on leftMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - Behavior on topMargin { - animation: Appearance.animation.elementMove.numberAnimation.createObject(this) - } - } - width: wallpaper.width - height: wallpaper.height - states: State { - name: "centered" - when: GlobalStates.screenLocked || bgRoot.wallpaperSafetyTriggered - PropertyChanges { - target: widgetCanvas - width: parent.width - height: parent.height - } - AnchorChanges { - target: widgetCanvas - anchors { - left: undefined - right: undefined - top: undefined - bottom: undefined - horizontalCenter: parent.horizontalCenter - verticalCenter: parent.verticalCenter - } - } + width: parent.width + height: parent.height + readonly property real parallaxFactor: { + var f = Config.options.background.parallax.widgetsFactor; + return f / Config.options.background.parallax.workspaceZoom; } + readonly property real baseWallpaperOffsetX: (bgRoot.screen.width - bgRoot.scaledWallpaperWidth) / 2 + readonly property real baseWallpaperOffsetY: (bgRoot.screen.height - bgRoot.scaledWallpaperHeight) / 2 + readonly property real wallpaperTotalOffsetX: wallpaper.x - baseWallpaperOffsetX + readonly property real wallpaperTotalOffsetY: wallpaper.y - baseWallpaperOffsetY + readonly property bool locked: GlobalStates.screenLocked + x: wallpaperTotalOffsetX * parallaxFactor * !locked + y: wallpaperTotalOffsetY * parallaxFactor * !locked + transitions: Transition { PropertyAnimation { properties: "width,height" @@ -275,9 +264,9 @@ Variants { sourceComponent: WeatherWidget { screenWidth: bgRoot.screen.width screenHeight: bgRoot.screen.height - scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale - scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale - wallpaperScale: bgRoot.effectiveWallpaperScale + scaledScreenWidth: bgRoot.screen.width + scaledScreenHeight: bgRoot.screen.height + wallpaperScale: 1 } } @@ -286,9 +275,9 @@ Variants { sourceComponent: ClockWidget { screenWidth: bgRoot.screen.width screenHeight: bgRoot.screen.height - scaledScreenWidth: bgRoot.screen.width / bgRoot.effectiveWallpaperScale - scaledScreenHeight: bgRoot.screen.height / bgRoot.effectiveWallpaperScale - wallpaperScale: bgRoot.effectiveWallpaperScale + scaledScreenWidth: bgRoot.screen.width + scaledScreenHeight: bgRoot.screen.height + wallpaperScale: 1 wallpaperSafetyTriggered: bgRoot.wallpaperSafetyTriggered } } diff --git a/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml new file mode 100644 index 000000000..cd1c6a202 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTextOverlay.qml @@ -0,0 +1,313 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Effects +import Qt5Compat.GraphicalEffects +import Quickshell + +import qs +import qs.modules.common +import qs.modules.common.functions +import qs.modules.common.models.gCloud +import qs.modules.common.utils +import qs.modules.common.widgets +import qs.services + +Item { + id: root + + property double scaleFactor: 1 + property color overlayColor: "#BB000000" + property color textColor: "white" + required property string screenshotPath + + readonly property string wikiLink: "https://ii.clsty.link/en/ii-qs/02usage/#setting-it-up" // TODO: write a page for this + readonly property string textColorDetectionScriptPath: Quickshell.shellPath("scripts/images/text-color-venv.sh") + + property bool loading: true + property var visionParagraphs: [] + property list translationKeys: [] + property var translation: ({}) + + function translate(s: string): string { + return translation[s] ?? s; + } + + property bool error: false + function showError() { + error = true; + } + + Component.onCompleted: { + if (GoogleCloud.tokenReady && GoogleCloud.tokenError) { + root.showError(); + } + cloudVision.annotateImage(screenshotPath); + } + + Connections { + target: GoogleCloud + function onTokenChanged() { + if (GoogleCloud.tokenReady && !GoogleCloud.tokenError) { + root.error = false; + cloudVision.annotateImage(root.screenshotPath); + } + } + } + + Rectangle { + id: loadingOverlay + anchors.fill: parent + opacity: root.loading ? 1 : 0 + Behavior on opacity { + animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this) + } + color: root.overlayColor + + Column { + visible: !root.error + anchors.centerIn: parent + spacing: 10 * root.scaleFactor + MaterialLoadingIndicator { + anchors.horizontalCenter: parent.horizontalCenter + implicitSize: 100 * root.scaleFactor + scale: 1 + ((1 - loadingOverlay.opacity) * 0.5) * root.scaleFactor + } + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + text: { + if (cloudVision.state == GCloudVision.State.Uploading) + return Translation.tr("Uploading image"); + else if (cloudVision.state == GCloudVision.State.Processing) + return Translation.tr("Reading image"); + else if (cloudVision.state == GCloudVision.State.Error) + return Translation.tr("Error"); + else if (cloudTrans.state == GCloudTranslate.State.Preparing) + return Translation.tr("Getting ready to translate"); + else if (cloudTrans.state == GCloudTranslate.State.Processing) + return Translation.tr("Translating"); + else + return " "; + } + font.pixelSize: Appearance.font.pixelSize.small * root.scaleFactor + animateChange: true + color: root.textColor + } + } + + Column { + visible: root.error + anchors.centerIn: parent + spacing: 10 * root.scaleFactor + + MaterialShapeWrappedMaterialSymbol { + anchors.horizontalCenter: parent.horizontalCenter + text: "exclamation" + iconSize: 80 * root.scaleFactor + padding: 6 * root.scaleFactor + color: Appearance.colors.colError + colSymbol: Appearance.colors.colOnError + shape: MaterialShape.Shape.Sunny + } + StyledText { + anchors.horizontalCenter: parent.horizontalCenter + horizontalAlignment: Text.AlignHCenter + textFormat: Text.MarkdownText + text: `**${Translation.tr("Screen Translator")}**\n\n${Translation.tr("Set your Google Cloud service account key")}\n\n__[${Translation.tr("See how on the wiki")}](${root.wikiLink})__` + font.pixelSize: Appearance.font.pixelSize.small * root.scaleFactor + color: root.textColor + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: { + Qt.openUrlExternally(root.wikiLink) + GlobalStates.screenTranslatorOpen = false + } + } + } + } + } + + GCloudVisionResult { + id: gcr + } + + GCloudVision { + id: cloudVision + onError: { + root.showError(); + } + onFinished: { + gcr.initializeWithData(outputData); + root.visionParagraphs = gcr.coherentParagraphs; + // print(gcr.coherentParagraphs) + root.translationKeys = gcr.coherentParagraphs.map(p => p.text); + // print("TRANSLATION KEYS:", JSON.stringify(root.translationKeys)); + cloudTrans.translateStrings(root.translationKeys); + } + } + + GCloudTranslate { + id: cloudTrans + onFinished: { + var values = outputData.translations.map(translation => translation.translatedText); + const keys = root.translationKeys; + root.translation = ({}); + for (var i = 0; i < keys.length; i++) { + Object.assign(root.translation, { + [keys[i]]: values[i] + }); + } + // print("TRANSLATION:", JSON.stringify(root.translation)); + root.loading = false; + } + } + + property real windowWidth: QsWindow.window.screen.width + property real windowHeight: QsWindow.window.screen.height + + StyledImage { + id: screenshotImage + z: 1 + asynchronous: false + width: root.windowWidth + height: root.windowHeight + sourceSize: Qt.size(root.windowWidth, root.windowHeight) + source: Qt.resolvedUrl(root.screenshotPath) + visible: false + } + + Item { + id: blurMaskItem + z: 2 + width: root.windowWidth + height: root.windowHeight + layer.enabled: true + visible: false + Repeater { + model: root.loading ? [] : root.visionParagraphs + delegate: VisionBoundingBoxRect { + readonly property string text: modelData.text + readonly property string translatedText: root.translate(text) + visible: translatedText != text + scaleFactor: 1 + } + } + } + + // I no longer need these but they were a fucking pain in the ass to figure out so they're staying + // GaussianBlur { + // id: blurredImage + // z: 3 + // width: root.windowWidth + // height: root.windowHeight + // transformOrigin: Item.TopLeft + // scale: root.scaleFactor + // source: screenshotImage + // radius: 10 + // samples: radius * 2 + 1 + // visible: false + // } + // MultiEffect { + // id: blurredImage + // z: 3 + // source: screenshotImage + // width: root.windowWidth + // height: root.windowHeight + // transformOrigin: Item.TopLeft + // scale: root.scaleFactor + + // blurEnabled: true + // blur: 1 + // blurMax: 64 + // visible: false + // } + + MaskMultiEffect { + z: 4 + implicitWidth: parent.width + implicitHeight: parent.height + width: parent.width + height: parent.height + + // Mask + source: screenshotImage + maskSource: blurMaskItem + + // Blur + blurEnabled: true + blur: 1 + blurMax: 50 + blurMultiplier: root.scaleFactor + autoPaddingEnabled: false + } + + Item { + id: textItems + z: 999 + Repeater { + model: root.loading ? [] : root.visionParagraphs + // An entry looks like this: + delegate: TextItem {} + } + } + + component VisionBoundingBoxRect: Rectangle { + required property var modelData + property real scaleFactor: root.scaleFactor + property list boundingVertices: modelData.boundingBox.vertices + property real unscaledX: boundingVertices[0].x + property real unscaledY: boundingVertices[0].y + property real unscaledWidth: boundingVertices[1].x - boundingVertices[0].x + property real unscaledHeight: boundingVertices[3].y - boundingVertices[0].y + x: unscaledX * scaleFactor + y: unscaledY * scaleFactor + width: unscaledWidth * scaleFactor + height: unscaledHeight * scaleFactor + radius: 4 + } + + component TextItem: VisionBoundingBoxRect { + id: ti + // {"boundingPoly": {"vertices": [{"x": 536,"y": 236},{"x": 583,"y": 236},{"x": 583,"y": 262},{"x": 536,"y": 262}]},"description": "宮坂"} + readonly property string text: modelData.text + readonly property string translatedText: root.translate(text) + visible: translatedText != text + + color: ColorUtils.transparentize(Appearance.colors.colSecondaryContainer, 0.4) + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + + Loader { + active: ti.visible + sourceComponent: MultiTurnProcess { + Component.onCompleted: { + runSequence([ // + [ // + "bash", "-c", // + `magick ${StringUtils.shellSingleQuoteEscape(root.screenshotPath)} +repage -crop ${StringUtils.shellSingleQuoteEscape(ti.unscaledWidth)}x${StringUtils.shellSingleQuoteEscape(ti.unscaledHeight)}+${StringUtils.shellSingleQuoteEscape(ti.unscaledX)}+${StringUtils.shellSingleQuoteEscape(ti.unscaledY)} png:- | ${root.textColorDetectionScriptPath}` + ], + (out => { + var colorData = JSON.parse(out); + ti.color = ColorUtils.transparentize(colorData.background, 0.4); + tiText.color = colorData.text; + }) + ]); + } + } + } + + SqueezedAnnotationStyledText { + id: tiText + width: parent.width + height: parent.height + text: ti.translatedText + scaleFactor: root.scaleFactor + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTranslator.qml b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTranslator.qml new file mode 100644 index 000000000..39db82909 --- /dev/null +++ b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTranslator.qml @@ -0,0 +1,41 @@ +pragma ComponentBehavior: Bound +import qs +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Scope { + id: root + + function dismiss() { + GlobalStates.screenTranslatorOpen = false + } + + Loader { + id: translatorLoader + active: GlobalStates.screenTranslatorOpen + + sourceComponent: ScreenTranslatorPanel { + onDismiss: root.dismiss() + } + } + + function translate() { + GlobalStates.screenTranslatorOpen = true + } + + IpcHandler { + target: "screenTranslator" + + function translate() { + root.translate() + } + } + + GlobalShortcut { + name: "screenTranslate" + description: "Translates screen content" + onPressed: root.translate() + } +} diff --git a/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTranslatorPanel.qml b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTranslatorPanel.qml new file mode 100644 index 000000000..ac4070ede --- /dev/null +++ b/dots/.config/quickshell/ii/modules/ii/screenTranslator/ScreenTranslatorPanel.qml @@ -0,0 +1,225 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Wayland + +import qs.modules.common +import qs.modules.common.utils +import qs.modules.common.widgets +import qs.services + +PanelWindow { + id: root + + // Interface + signal dismiss + + // Window props + visible: false + // color: Appearance.colors.colLayer0 + color: "black" + WlrLayershell.namespace: "quickshell:regionSelector" + WlrLayershell.layer: WlrLayer.Overlay + WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand + exclusionMode: ExclusionMode.Ignore + anchors { + left: true + right: true + top: true + bottom: true + } + + // Config + readonly property string screenshotDir: Directories.screenshotTemp + readonly property string screenshotPath: `${root.screenshotDir}/image-${screen.name}` + + // Preparation + property bool screenshotReady: false + + function performTranslation() { + screenshotReady = true; + } + + TempScreenshotProcess { + id: screenshotProc + running: true + screen: root.screen + screenshotDir: root.screenshotDir + screenshotPath: root.screenshotPath + onExited: (_, __) => { + root.visible = true; + root.performTranslation(); + } + } + + // Actual content + property real scale: 1.0 + property real contentX: 0 + property real contentY: 0 + + MouseArea { + anchors.fill: parent + clip: true + + property real lastX: 0 + property real lastY: 0 + + cursorShape: Qt.SizeAllCursor + + onPressed: mouse => { + lastX = mouse.x; + lastY = mouse.y; + } + + onPositionChanged: mouse => { + if (pressed) { + root.contentX += (mouse.x - lastX); + root.contentY += (mouse.y - lastY); + lastX = mouse.x; + lastY = mouse.y; + } + } + + onWheel: event => { + const zoomFactor = event.angleDelta.y > 0 ? 1.1 : 0.9; + const oldScale = root.scale; + const newScale = Math.min(Math.max(0.1, oldScale * zoomFactor), 5); + + if (newScale !== oldScale) { + // Determine mouse position relative to the content's unscaled origin + const localX = (event.x - root.contentX) / oldScale; + const localY = (event.y - root.contentY) / oldScale; + + // Apply zoom + root.scale = newScale; + + // Shift offsets to keep the same local point under the cursor + root.contentX = event.x - (localX * newScale); + root.contentY = event.y - (localY * newScale); + } + } + + ScreencopyView { // Freeze screen + id: screencopy + width: parent.width + height: parent.height + + x: root.contentX + y: root.contentY + scale: root.scale + transformOrigin: Item.TopLeft + + live: false + captureSource: root.screen + } + + Loader { + width: parent.width * root.scale + height: parent.height * root.scale + + x: root.contentX + y: root.contentY + + active: root.screenshotReady + sourceComponent: ScreenTextOverlay { + screenshotPath: root.screenshotPath + scaleFactor: root.scale + } + } + } + + Row { + anchors { + horizontalCenter: parent.horizontalCenter + bottom: parent.bottom + bottomMargin: -height + } + Behavior on anchors.bottomMargin { + animation: Appearance.animation.elementMove.numberAnimation.createObject(this) + } + Component.onCompleted: { + anchors.bottomMargin = 8; + } + + spacing: 6 + + Toolbar { + id: toolbar + focus: root.visible + Keys.onPressed: event => { // Esc to close + if (event.key === Qt.Key_Escape) { + root.dismiss(); + } + } + spacing: 0 + + IconToolbarButton { + id: sleepButton + onClicked: { + toggled = !toggled + if (toggled) keyInput.forceActiveFocus() + } + text: "key" + + StyledToolTip { + z: 9999 + text: Translation.tr("Key input") + } + } + + Revealer { + reveal: sleepButton.toggled + Layout.fillHeight: true + + RowLayout { + anchors.left: parent.left + spacing: 6 + Item {} // extra padding + ToolbarTextField { + id: keyInput + implicitWidth: 400 + placeholderText: Translation.tr("Paste service account key JSON here") + inputMethodHints: Qt.ImhSensitiveData + onAccepted: submit() + + function submit() { + const success = GoogleCloud.setKeyJson(text); + if (!success) { + invalidJsonAnimation.restart(); + } else { + text = ""; + sleepButton.toggled = false; + } + } + + ErrorShakeAnimation { + id: invalidJsonAnimation + target: keyInput + } + } + IconToolbarButton { + id: submitButton + onClicked: keyInput.submit() + text: "check" + toggled: keyInput.text.length > 0 + + StyledToolTip { + z: 9999 + text: Translation.tr("Confirm") + } + } + } + } + } + + ToolbarPairedFab { + iconText: "close" + onClicked: root.dismiss() + StyledToolTip { + text: Translation.tr("Close") + } + } + } +} diff --git a/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml b/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml index 78e0473a2..680421f03 100644 --- a/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml +++ b/dots/.config/quickshell/ii/modules/settings/BackgroundConfig.qml @@ -43,8 +43,8 @@ ContentPage { icon: "loupe" text: Translation.tr("Preferred wallpaper zoom (%)") value: Config.options.background.parallax.workspaceZoom * 100 - from: 100 - to: 150 + from: 10 + to: 200 stepSize: 1 onValueChanged: { Config.options.background.parallax.workspaceZoom = value / 100; diff --git a/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml b/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml index f4ffda651..61a901fa2 100644 --- a/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml +++ b/dots/.config/quickshell/ii/panelFamilies/IllogicalImpulseFamily.qml @@ -15,6 +15,7 @@ import qs.modules.ii.overview import qs.modules.ii.polkit import qs.modules.ii.regionSelector import qs.modules.ii.screenCorners +import qs.modules.ii.screenTranslator import qs.modules.ii.sessionScreen import qs.modules.ii.sidebarLeft import qs.modules.ii.sidebarRight @@ -37,6 +38,7 @@ Scope { PanelLoader { component: Polkit {} } PanelLoader { component: RegionSelector {} } PanelLoader { component: ScreenCorners {} } + PanelLoader { component: ScreenTranslator {} } PanelLoader { component: SessionScreen {} } PanelLoader { component: SidebarLeft {} } PanelLoader { component: SidebarRight {} } diff --git a/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml b/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml index 67d35de55..1953703b4 100644 --- a/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml +++ b/dots/.config/quickshell/ii/panelFamilies/WaffleFamily.qml @@ -20,6 +20,7 @@ import qs.modules.waffle.taskView import qs.modules.ii.cheatsheet import qs.modules.ii.onScreenKeyboard import qs.modules.ii.overlay +import qs.modules.ii.screenTranslator import qs.modules.ii.wallpaperSelector Scope { @@ -40,5 +41,6 @@ Scope { PanelLoader { component: Cheatsheet {} } PanelLoader { component: OnScreenKeyboard {} } PanelLoader { component: Overlay {} } + PanelLoader { component: ScreenTranslator {} } PanelLoader { component: WallpaperSelector {} } } diff --git a/dots/.config/quickshell/ii/scripts/images/text-color-venv.sh b/dots/.config/quickshell/ii/scripts/images/text-color-venv.sh new file mode 100755 index 000000000..f673bd687 --- /dev/null +++ b/dots/.config/quickshell/ii/scripts/images/text-color-venv.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate +"$SCRIPT_DIR/text_color.py" "$@" +deactivate diff --git a/dots/.config/quickshell/ii/scripts/images/text_color.py b/dots/.config/quickshell/ii/scripts/images/text_color.py new file mode 100755 index 000000000..3fcff8d10 --- /dev/null +++ b/dots/.config/quickshell/ii/scripts/images/text_color.py @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# Disclaimer: This script was ai-generated and went through minimal revision. + +import cv2 +import numpy as np +import json +import sys + +def to_hex(color): + return "#{:02x}{:02x}{:02x}".format(int(color[0]), int(color[1]), int(color[2])) + +def get_color_from_stdin(): + # Read raw bytes from stdin + input_data = sys.stdin.buffer.read() + if not input_data: + return {"error": "No data received via stdin"} + + # Convert bytes to numpy array and decode to image + nparr = np.frombuffer(input_data, np.uint8) + img = cv2.imdecode(nparr, cv2.IMREAD_COLOR) + + if img is None: + return {"error": "Could not decode image data"} + + img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + h, w, _ = img_rgb.shape + + # 1. Sample corner pixels (The background anchors) + corners = np.array([ + img_rgb[0, 0], + img_rgb[0, w-1], + img_rgb[h-1, 0], + img_rgb[h-1, w-1] + ]) + + # 2. Determine single dominant background + # Using median handles noise/gradients better than a simple average + bg_color = np.median(corners, axis=0).astype(int) + + # 3. Find the Text Color + pixels = img_rgb.reshape(-1, 3).astype(int) + distances = np.linalg.norm(pixels - bg_color, axis=1) + + # Take the 95th percentile of pixels furthest from background + threshold = np.percentile(distances, 95) + text_pixels = pixels[distances >= threshold] + + if len(text_pixels) == 0: + text_color = [255, 255, 255] # Fallback + else: + text_color = np.median(text_pixels, axis=0).astype(int) + + return { + "background": to_hex(bg_color), + "text": to_hex(text_color) + } + +if __name__ == "__main__": + result = get_color_from_stdin() + print(json.dumps(result)) \ No newline at end of file diff --git a/dots/.config/quickshell/ii/services/GoogleCloud.qml b/dots/.config/quickshell/ii/services/GoogleCloud.qml new file mode 100644 index 000000000..e90ae5dfd --- /dev/null +++ b/dots/.config/quickshell/ii/services/GoogleCloud.qml @@ -0,0 +1,93 @@ +pragma Singleton +pragma ComponentBehavior: Bound +import QtQuick +import Quickshell +import qs.modules.common.utils + +Singleton { + id: root + + property var keyContent: ({}) + property string keyProjectId: keyContent.project_id + property bool keyError: false + property bool keyReady: false + property string token: "" + property bool tokenError: false + property bool tokenReady: false + readonly property string projectId: keyProjectId + + readonly property bool loaded: keyReady && tokenReady + + readonly property string tokenForKeyScriptPath: Quickshell.shellPath("services/gCloud/token-from-key-venv.sh") + + function load() { + // Dummy for init + } + + function setKeyJson(str: string): bool { + try { + var keyData = JSON.parse(str) + KeyringStorage.setNestedField(["googleCloud", "serviceAccountKey"], keyData); + return true; + } catch(e) { + return false; + } + } + + function getToken() { + if (root.keyError) { + root.tokenError = true; + root.tokenReady = true; + return; + } + tokenProc.runSequence([(() => { // prep token fetcher + tokenProc.environment.SERVICE_KEY_CONTENT = JSON.stringify(root.keyContent); + tokenProc.command = [ // + "bash", "-c" // + , `${tokenForKeyScriptPath} "$SERVICE_KEY_CONTENT"`]; + }), [] // run token fetcher + , (out => { + if (out.startsWith("Error")) { + root.tokenError = true; + } else { + root.tokenError = false; + root.token = out.trim(); + } + root.tokenReady = true; + })]); + } + + function loadKeyIfPossible() { + if (KeyringStorage.loaded) { + root.keyContent = KeyringStorage.keyringData?.googleCloud?.serviceAccountKey; + if (!root.keyContent?.project_id) { + root.keyError = true; + } else { + root.keyError = false; + root.keyProjectId = root.keyContent.project_id; + } + root.keyReady = true; + root.getToken(); + } else { + KeyringStorage.fetchKeyringData(); + } + } + + Component.onCompleted: { + loadKeyIfPossible(); + } + + Connections { + target: KeyringStorage + function onLoadedChanged() { + root.loadKeyIfPossible(); + } + function onDataChanged() { + root.loadKeyIfPossible(); + } + } + + MultiTurnProcess { + id: tokenProc + } +} diff --git a/dots/.config/quickshell/ii/services/KeyringStorage.qml b/dots/.config/quickshell/ii/services/KeyringStorage.qml index ae49496d2..7e716d459 100644 --- a/dots/.config/quickshell/ii/services/KeyringStorage.qml +++ b/dots/.config/quickshell/ii/services/KeyringStorage.qml @@ -15,6 +15,8 @@ import QtQuick; Singleton { id: root + signal dataChanged() + property bool loaded: false property var keyringData: ({}) @@ -82,6 +84,7 @@ Singleton { if (saveData.running) { // console.log("[KeyringStorage] Saving with command: '" + saveData.command.join("' '") + "'"); saveData.write(JSON.stringify(root.keyringData)); + root.dataChanged() stdinEnabled = false // End input stream } } diff --git a/dots/.config/quickshell/ii/services/gCloud/token-from-key-venv.sh b/dots/.config/quickshell/ii/services/gCloud/token-from-key-venv.sh new file mode 100755 index 000000000..fd9e255e8 --- /dev/null +++ b/dots/.config/quickshell/ii/services/gCloud/token-from-key-venv.sh @@ -0,0 +1,6 @@ +#!/usr/bin/env bash +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + +source $(eval echo $ILLOGICAL_IMPULSE_VIRTUAL_ENV)/bin/activate +"$SCRIPT_DIR/token_from_key.py" "$@" +deactivate diff --git a/dots/.config/quickshell/ii/services/gCloud/token_from_key.py b/dots/.config/quickshell/ii/services/gCloud/token_from_key.py new file mode 100755 index 000000000..f40a50661 --- /dev/null +++ b/dots/.config/quickshell/ii/services/gCloud/token_from_key.py @@ -0,0 +1,30 @@ +#!/usr/bin/env python3 +import sys +import json +import google.auth.transport.requests +import google.oauth2.service_account + +def get_token(json_str): + try: + # Load the string into a dictionary + info = json.loads(json_str) + + # Initialize credentials + creds = google.oauth2.service_account.Credentials.from_service_account_info(info) + scoped_creds = creds.with_scopes(['https://www.googleapis.com/auth/cloud-platform']) + + # Refresh to get the access token + request = google.auth.transport.requests.Request() + scoped_creds.refresh(request) + + print(scoped_creds.token) + except Exception as e: + sys.stderr.write(f"Error: {str(e)}\n") + sys.exit(1) + +if __name__ == "__main__": + if len(sys.argv) < 2: + sys.stderr.write("Usage: python3 get_token.py ''\n") + sys.exit(1) + + get_token(sys.argv[1]) diff --git a/sdata/dist-arch/install-deps.sh b/sdata/dist-arch/install-deps.sh index 864c9cd2f..4e28cf23b 100644 --- a/sdata/dist-arch/install-deps.sh +++ b/sdata/dist-arch/install-deps.sh @@ -94,8 +94,7 @@ metapkgs=(./sdata/dist-arch/illogical-impulse-{audio,backlight,basic,fonts-theme metapkgs+=(./sdata/dist-arch/illogical-impulse-hyprland) metapkgs+=(./sdata/dist-arch/illogical-impulse-microtex-git) metapkgs+=(./sdata/dist-arch/illogical-impulse-quickshell-git) -[[ -f /usr/share/icons/Bibata-Modern-Classic/index.theme ]] || \ - metapkgs+=(./sdata/dist-arch/illogical-impulse-bibata-modern-classic-bin) +metapkgs+=(./sdata/dist-arch/illogical-impulse-bibata-modern-classic-bin) for i in "${metapkgs[@]}"; do metainstallflags="--needed" diff --git a/sdata/dist-gentoo/illogical-impulse-oneui4-icons-git/illogical-impulse-oneui4-icons-git-1.0-r1.ebuild b/sdata/dist-gentoo/illogical-impulse-oneui4-icons-git/illogical-impulse-oneui4-icons-git-1.0-r1.ebuild deleted file mode 100644 index 4e38d2e8b..000000000 --- a/sdata/dist-gentoo/illogical-impulse-oneui4-icons-git/illogical-impulse-oneui4-icons-git-1.0-r1.ebuild +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2025 Gentoo Authors -# Distributed under the terms of the GNU General Public License v2 - -EAPI=8 - -DESCRIPTION="A fork of mjkim0727/OneUI4-Icons for illogical-impulse dotfiles" -HOMEPAGE="" -SRC_URI="https://github.com/end-4/OneUI4-Icons/archive/main.tar.gz -> ${P}.tar.gz" - -LICENSE="GPL-3" -SLOT="0" -KEYWORDS="~amd64 ~arm64 ~x86" -RESTRICT="strip" - -DEPEND="" -RDEPEND="" - -S="${WORKDIR}/OneUI4-Icons-main" - -src_install() { - insinto /usr/share/icons - - for theme in "OneUI" "OneUI-dark" "OneUI-light"; do - doins -r ${S}/${theme} - done -} diff --git a/sdata/dist-gentoo/install-deps.sh b/sdata/dist-gentoo/install-deps.sh index 1cbbdb966..e27af481e 100644 --- a/sdata/dist-gentoo/install-deps.sh +++ b/sdata/dist-gentoo/install-deps.sh @@ -33,7 +33,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,quickshell-git,screencapture,toolkit,widgets}) +metapkgs=(illogical-impulse-{audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,portal,python,quickshell-git,screencapture,toolkit,widgets}) ebuild_dir="/var/db/repos/ii-dots" diff --git a/sdata/dist-gentoo/keywords b/sdata/dist-gentoo/keywords index 7e6fe2ab2..b6417ab31 100644 --- a/sdata/dist-gentoo/keywords +++ b/sdata/dist-gentoo/keywords @@ -6,7 +6,6 @@ app-misc/illogical-impulse-fonts-themes app-misc/illogical-impulse-hyprland app-misc/illogical-impulse-kde 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 diff --git a/sdata/dist-gentoo/uninstall-deps.sh b/sdata/dist-gentoo/uninstall-deps.sh index 280433946..849880445 100644 --- a/sdata/dist-gentoo/uninstall-deps.sh +++ b/sdata/dist-gentoo/uninstall-deps.sh @@ -1,7 +1,7 @@ # This script is meant to be sourced. # It's not for directly running. -for i in illogical-impulse-{quickshell-git,audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,oneui4-icons-git,portal,python,screencapture,toolkit,widgets}; do +for i in illogical-impulse-{quickshell-git,audio,backlight,basic,bibata-modern-classic-bin,fonts-themes,hyprland,kde,microtex-git,portal,python,screencapture,toolkit,widgets}; do v sudo emerge --unmerge $i done diff --git a/sdata/uv/requirements.in b/sdata/uv/requirements.in index 277a461cb..58023b915 100644 --- a/sdata/uv/requirements.in +++ b/sdata/uv/requirements.in @@ -16,3 +16,5 @@ pygobject tqdm numpy opencv-contrib-python +google-auth +requests diff --git a/sdata/uv/requirements.txt b/sdata/uv/requirements.txt index f58e87d3d..cdfd65503 100644 --- a/sdata/uv/requirements.txt +++ b/sdata/uv/requirements.txt @@ -1,63 +1,83 @@ # This file was autogenerated by uv via the following command: -# uv pip compile sdata/uv/requirements.in -o sdata/uv/requirements.txt +# uv pip compile requirements.in -o requirements.txt build==1.2.2.post1 - # via -r sdata/uv/requirements.in + # via -r requirements.in +certifi==2026.2.25 + # via requests cffi==1.17.1 - # via pywayland + # via + # cryptography + # pywayland +charset-normalizer==3.4.7 + # via requests click==8.2.1 - # via -r sdata/uv/requirements.in + # via -r requirements.in +cryptography==46.0.0 + # via google-auth dbus-python==1.4.0 # via kde-material-you-colors +google-auth==2.49.1 + # via -r requirements.in +idna==3.11 + # via requests kde-material-you-colors==1.10.1 - # via -r sdata/uv/requirements.in + # via -r requirements.in libsass==0.23.0 - # via -r sdata/uv/requirements.in + # via -r requirements.in loguru==0.7.3 - # via -r sdata/uv/requirements.in + # via -r requirements.in material-color-utilities==0.2.1 - # via -r sdata/uv/requirements.in + # via -r requirements.in materialyoucolor==2.0.10 # via - # -r sdata/uv/requirements.in + # -r requirements.in # kde-material-you-colors numpy==2.2.2 # via - # -r sdata/uv/requirements.in + # -r requirements.in # kde-material-you-colors # material-color-utilities # opencv-contrib-python opencv-contrib-python==4.12.0.88 - # via -r sdata/uv/requirements.in + # via -r requirements.in packaging==24.2 # via # build # setuptools-scm pillow==11.1.0 # via - # -r sdata/uv/requirements.in + # -r requirements.in # kde-material-you-colors # material-color-utilities psutil==6.1.1 - # via -r sdata/uv/requirements.in + # via -r requirements.in +pyasn1==0.6.3 + # via pyasn1-modules +pyasn1-modules==0.4.2 + # via google-auth pycairo==1.28.0 # via - # -r sdata/uv/requirements.in + # -r requirements.in # pygobject pycparser==2.22 # via cffi pygobject==3.52.3 - # via -r sdata/uv/requirements.in + # via -r requirements.in pyproject-hooks==1.2.0 # via build pywayland==0.4.18 - # via -r sdata/uv/requirements.in + # via -r requirements.in +requests==2.33.1 + # via -r requirements.in setproctitle==1.3.4 - # via -r sdata/uv/requirements.in + # via -r requirements.in setuptools==80.9.0 # via setuptools-scm setuptools-scm==8.1.0 - # via -r sdata/uv/requirements.in + # via -r requirements.in tqdm==4.67.1 - # via -r sdata/uv/requirements.in + # via -r requirements.in +urllib3==2.6.3 + # via requests wheel==0.45.1 - # via -r sdata/uv/requirements.in + # via -r requirements.in