forked from Shinonome/dots-hyprland
314 lines
11 KiB
QML
314 lines
11 KiB
QML
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<string> 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<var> 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)
|
|
}
|
|
}
|
|
}
|
|
}
|