work safety for clipboard images copied from browser

This commit is contained in:
end-4
2025-09-26 23:56:13 +02:00
parent 01815d04dc
commit 1e175e4e82
5 changed files with 120 additions and 53 deletions
@@ -42,7 +42,12 @@ Variants {
// 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
property bool wallpaperSafetyTriggered: Config.options.background.wallpaperSafety.enable && (CF.StringUtils.stringListContainsSubstring(wallpaperPath.toLowerCase(), Config.options.background.wallpaperSafety.triggerCondition.wallpaperKeywords) && CF.StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.background.wallpaperSafety.triggerCondition.networkNameKeywords))
property bool wallpaperSafetyTriggered: {
const enabled = Config.options.workSafety.enable.wallpaper
const sensitiveWallpaper = (CF.StringUtils.stringListContainsSubstring(wallpaperPath.toLowerCase(), Config.options.workSafety.triggerCondition.fileKeywords))
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
property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated
@@ -141,13 +141,6 @@ Singleton {
property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
property bool enableSidebar: true
}
property JsonObject wallpaperSafety: JsonObject {
property bool enable: true
property JsonObject triggerCondition: JsonObject {
property list<string> wallpaperKeywords: ["anime", "ecchi", "hentai", "yande.re", "konachan", "breast", "nipples", "pussy", "nsfw", "spoiler", "girl"]
property list<string> networkNameKeywords: ["airport", "cafe", "college", "company", "eduroam", "free", "guest", "public", "school", "university"]
}
}
}
property JsonObject bar: JsonObject {
@@ -379,6 +372,18 @@ Singleton {
property JsonObject screenshotTool: JsonObject {
property bool showContentRegions: true
}
property JsonObject workSafety: JsonObject {
property JsonObject enable: JsonObject {
property bool wallpaper: true
property bool clipboard: true
}
property JsonObject triggerCondition: JsonObject {
property list<string> networkNameKeywords: ["airport", "cafe", "college", "company", "eduroam", "free", "guest", "public", "school", "university"]
property list<string> fileKeywords: ["anime", "ecchi", "hentai", "yande.re", "konachan", "breast", "nipples", "pussy", "nsfw", "spoiler", "girl"]
property list<string> linkKeywords: ["hentai", "porn", "sukebei", "hitomi.la", "rule34", "gelbooru", "fanbox", "dlsite"]
}
}
}
}
}
@@ -12,6 +12,8 @@ Rectangle {
property string entry
property real maxWidth
property real maxHeight
property bool blur: false
property string blurText: "Image hidden"
property string imageDecodePath: Directories.cliphistDecode
property string imageDecodeFileName: `${entryNumber}`
@@ -19,26 +21,25 @@ Rectangle {
property string source
property int entryNumber: {
if (!root.entry) return 0
const match = root.entry.match(/^(\d+)\t/)
return match ? parseInt(match[1]) : 0
if (!root.entry)
return 0;
const match = root.entry.match(/^(\d+)\t/);
return match ? parseInt(match[1]) : 0;
}
property int imageWidth: {
if (!root.entry) return 0
const match = root.entry.match(/(\d+)x(\d+)/)
return match ? parseInt(match[1]) : 0
if (!root.entry)
return 0;
const match = root.entry.match(/(\d+)x(\d+)/);
return match ? parseInt(match[1]) : 0;
}
property int imageHeight: {
if (!root.entry) return 0
const match = root.entry.match(/(\d+)x(\d+)/)
return match ? parseInt(match[2]) : 0
if (!root.entry)
return 0;
const match = root.entry.match(/(\d+)x(\d+)/);
return match ? parseInt(match[2]) : 0;
}
property real scale: {
return Math.min(
root.maxWidth / imageWidth,
root.maxHeight / imageHeight,
1
)
return Math.min(root.maxWidth / imageWidth, root.maxHeight / imageHeight, 1);
}
color: Appearance.colors.colLayer1
@@ -47,26 +48,33 @@ Rectangle {
implicitWidth: imageWidth * scale
Component.onCompleted: {
decodeImageProcess.running = true
decodeImageProcess.running = true;
}
Process {
id: decodeImageProcess
command: ["bash", "-c",
`[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`
]
command: ["bash", "-c", `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | ${Cliphist.cliphistBinary} decode > '${imageDecodeFilePath}'`]
onExited: (exitCode, exitStatus) => {
if (exitCode === 0) {
root.source = imageDecodeFilePath
root.source = imageDecodeFilePath;
} else {
console.error("[CliphistImage] Failed to decode image for entry:", root.entry)
root.source = ""
console.error("[CliphistImage] Failed to decode image for entry:", root.entry);
root.source = "";
}
}
}
Component.onDestruction: {
Quickshell.execDetached(["bash", "-c", `[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'`])
Quickshell.execDetached(["bash", "-c", `[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'`]);
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: image.width
height: image.height
radius: root.radius
}
}
Image {
@@ -82,15 +90,42 @@ Rectangle {
height: root.imageHeight * root.scale
sourceSize.width: width
sourceSize.height: height
}
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: image.width
height: image.height
radius: root.radius
Loader {
id: blurLoader
active: root.blur
anchors.fill: image
sourceComponent: GaussianBlur {
source: image
radius: 35
samples: radius * 2 + 1
Rectangle {
anchors.fill: parent
color: ColorUtils.transparentize(Appearance.colors.colLayer0, 0.5)
Column {
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
MaterialSymbol {
visible: width <= image.width
anchors.horizontalCenter: parent.horizontalCenter
text: "visibility_off"
font.pixelSize: 28
}
StyledText {
visible: width <= image.width
anchors.horizontalCenter: parent.horizontalCenter
text: root.blurText
color: Appearance.colors.colOnSurface
font.pixelSize: Appearance.font.pixelSize.smallie
}
}
}
}
}
}
@@ -24,6 +24,8 @@ RippleButton {
property string bigText: entry?.bigText ?? ""
property string materialSymbol: entry?.materialSymbol ?? ""
property string cliphistRawString: entry?.cliphistRawString ?? ""
property bool blurImage: entry?.blurImage ?? false
property string blurImageText: entry?.blurImageText ?? "Image hidden"
visible: root.entryShown
property int horizontalMargin: 10
@@ -208,6 +210,8 @@ RippleButton {
entry: root.cliphistRawString
maxWidth: contentColumn.width
maxHeight: 140
blur: root.blurImage
blurText: root.blurImageText
}
}
}
@@ -20,20 +20,10 @@ Item { // Wrapper
implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2
property string mathResult: ""
function disableExpandAnimation() {
searchWidthBehavior.enabled = false;
}
function cancelSearch() {
searchInput.selectAll();
root.searchingText = "";
searchWidthBehavior.enabled = true;
}
function setSearchingText(text) {
searchInput.text = text;
root.searchingText = text;
property bool clipboardWorkSafetyActive: {
const enabled = Config.options.workSafety.enable.clipboard;
const sensitiveNetwork = (StringUtils.stringListContainsSubstring(Network.networkName.toLowerCase(), Config.options.workSafety.triggerCondition.networkNameKeywords))
return enabled && sensitiveNetwork;
}
property var searchActions: [
@@ -97,6 +87,27 @@ Item { // Wrapper
appResults.currentIndex = 0;
}
function disableExpandAnimation() {
searchWidthBehavior.enabled = false;
}
function cancelSearch() {
searchInput.selectAll();
root.searchingText = "";
searchWidthBehavior.enabled = true;
}
function setSearchingText(text) {
searchInput.text = text;
root.searchingText = text;
}
function containsUnsafeLink(entry) {
if (entry == undefined) return false;
const unsafeKeywords = Config.options.workSafety.triggerCondition.linkKeywords;
return StringUtils.stringListContainsSubstring(entry.toLowerCase(), unsafeKeywords);
}
Timer {
id: nonAppResultsTimer
interval: Config.options.search.nonAppResultDelay
@@ -311,7 +322,12 @@ Item { // Wrapper
if (root.searchingText.startsWith(Config.options.search.prefix.clipboard)) {
// Clipboard
const searchString = root.searchingText.slice(Config.options.search.prefix.clipboard.length);
return Cliphist.fuzzyQuery(searchString).map(entry => {
return Cliphist.fuzzyQuery(searchString).map((entry, index, array) => {
const mightBlurImage = Cliphist.entryIsImage(entry) && root.clipboardWorkSafetyActive;
let shouldBlurImage = mightBlurImage;
if (mightBlurImage) {
shouldBlurImage = shouldBlurImage && (containsUnsafeLink(array[index - 1]) || containsUnsafeLink(array[index + 1]));
}
return {
cliphistRawString: entry,
name: StringUtils.cleanCliphistEntry(entry),
@@ -335,7 +351,9 @@ Item { // Wrapper
Cliphist.deleteEntry(entry);
}
}
]
],
blurImage: shouldBlurImage,
blurImageText: Translation.tr("Work safety")
};
}).filter(Boolean);
}