Skip to content

Commit

Permalink
feat: add camera addon
Browse files Browse the repository at this point in the history
  • Loading branch information
CyanSalt committed Sep 29, 2024
1 parent 7e58573 commit c74721f
Show file tree
Hide file tree
Showing 7 changed files with 264 additions and 2 deletions.
10 changes: 10 additions & 0 deletions addons/camera/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* @param {import('@commas/api/types').API} commas
*/
module.exports = function (commas) {
if (commas.app.isMainProcess()) {
require('./dist/main').default()
} else {
require('./dist/renderer').default()
}
}
19 changes: 19 additions & 0 deletions addons/camera/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "@commas/camera",
"private": true,
"version": "0.1.0",
"productName": "Camera",
"description": "Capture page contents",
"main": "index.js",
"author": "commas",
"license": "ISC",
"commas:icon": {
"name": "lucide-camera"
},
"commas:i18n": {
"zh-CN": {
"productName": "照相机",
"description": "捕获页面内容"
}
}
}
16 changes: 16 additions & 0 deletions addons/camera/src/main/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import * as commas from 'commas:api/main'
import type { NativeImage, Rectangle } from 'electron'

declare module '@commas/electron-ipc' {
export interface Commands {
'capture-page': (rect: Rectangle) => NativeImage,
}
}

export default () => {

commas.ipcMain.handle('capture-page', async (event, rect) => {
return event.sender.capturePage(rect)
})

}
197 changes: 197 additions & 0 deletions addons/camera/src/renderer/CameraAnchor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
<script lang="ts" setup>
import { ipcRenderer } from '@commas/electron-ipc'
import { refAutoReset } from '@vueuse/core'
import * as commas from 'commas:api/renderer'
import { clipboard, nativeImage } from 'electron'
import icon from './assets/icon-stroked.svg'
defineOptions({
inheritAttrs: false,
})
const { VisualIcon } = commas.ui.vueAssets
const theme = commas.remote.useTheme()
let feedbacking = $(refAutoReset(false, 1000))
function createCanvas(width: number, height: number) {
const canvas = document.createElement('canvas')
canvas.width = width
canvas.height = height
const context = canvas.getContext('2d')!
return { canvas, context }
}
function loadingElement<T extends HTMLElement>(element: T) {
return new Promise<T>((resolve, reject) => {
function handleLoad() {
dispose()
resolve(element)
}
function handleError(event: ErrorEvent) {
reject(event.error)
}
function dispose() {
element.removeEventListener('load', handleLoad)
element.removeEventListener('error', handleError)
}
element.addEventListener('load', handleLoad, { once: true })
element.addEventListener('error', handleError, { once: true })
})
}
async function loadImage(url: string) {
const image = new Image()
image.src = url
return loadingElement(image)
}
interface Rect {
x: number,
y: number,
width: number,
height: number,
}
function roundCorners(
context: CanvasRenderingContext2D,
rect: Rect,
radius: number,
) {
const { x, y, width, height } = rect
context.moveTo(x + radius, y)
context.lineTo(x + width - radius, y)
context.quadraticCurveTo(x + width, y, x + width, y + radius)
context.lineTo(x + width, y + height - radius)
context.quadraticCurveTo(x + width, y + height, x + width - radius, y + height)
context.lineTo(x + radius, y + height)
context.quadraticCurveTo(x, y + height, x, y + height - radius)
context.lineTo(x, y + radius)
context.quadraticCurveTo(x, y, x + radius, y)
}
async function createIconCanvas(color: string) {
const size = 24 * devicePixelRatio
const { canvas, context } = createCanvas(size, size)
const image = await loadImage(icon)
context.drawImage(image, 0, 0, canvas.width, canvas.height)
const data = context.getImageData(0, 0, canvas.width, canvas.height)
const bytes = data.data
const rgba = commas.helper.toRGBA(color)
for (let i = 0; i < bytes.length; i += 4) {
bytes[i] = rgba.r
bytes[i + 1] = rgba.g
bytes[i + 2] = rgba.b
bytes[i + 3] = bytes[i + 3] * rgba.a
}
context.putImageData(data, 0, 0)
return canvas
}
async function capture() {
const activeElement = document.querySelector('.terminal-view > .active')
if (activeElement) {
const { x, y, width, height } = activeElement.getBoundingClientRect()
const screenshot = await ipcRenderer.invoke('capture-page', { x, y, width, height })
const size = screenshot.getSize()
const { canvas, context } = createCanvas(
size.width + 128 * devicePixelRatio,
size.height + 128 * devicePixelRatio,
)
const watermarkColor = 'rgb(255 255 255 / 50%)'
const iconCanvas = await createIconCanvas(watermarkColor)
const screenshotImage = await loadImage(screenshot.toDataURL())
// Gradient background
context.save()
const gradient = context.createLinearGradient(0, 0, canvas.width, canvas.height)
const systemAccentRGBA = commas.helper.toRGBA(theme.systemAccent)
const backgroundRGBA = commas.helper.toRGBA(theme.background)
gradient.addColorStop(0, commas.helper.toCSSHEX(commas.helper.mix(systemAccentRGBA, backgroundRGBA, 0.12)))
gradient.addColorStop(1, commas.helper.toCSSHEX(commas.helper.mix(systemAccentRGBA, backgroundRGBA, 0.48)))
context.fillStyle = gradient
context.fillRect(0, 0, canvas.width, canvas.height)
context.restore()
// Translucent border
context.save()
context.beginPath()
context.fillStyle = commas.helper.toCSSHEX({ ...backgroundRGBA, a: 0.5 })
context.shadowOffsetX = 0
context.shadowOffsetY = 2 * devicePixelRatio
context.shadowBlur = 4 * devicePixelRatio
context.shadowColor = 'rgb(0 0 0 / 10%)'
roundCorners(
context,
{
x: (canvas.width - (size.width + 8 * devicePixelRatio)) / 2,
y: (canvas.height - (size.height + 8 * devicePixelRatio)) / 2,
width: size.width + 8 * devicePixelRatio,
height: size.height + 8 * devicePixelRatio,
},
8 * devicePixelRatio,
)
context.fill()
context.closePath()
context.clip()
context.restore()
// Screenshot
context.save()
context.beginPath()
context.fillStyle = theme.background
roundCorners(
context,
{
x: (canvas.width - size.width) / 2,
y: (canvas.height - size.height) / 2,
width: size.width,
height: size.height,
},
8 * devicePixelRatio,
)
context.fill()
context.closePath()
context.clip()
context.drawImage(
screenshotImage,
(canvas.width - size.width) / 2,
(canvas.height - size.height) / 2,
)
context.restore()
// Watermark text
context.save()
context.font = 'bold 24px system-ui'
context.fillStyle = watermarkColor
context.textBaseline = 'middle'
const text = 'Commas'
const metrics = context.measureText(text)
const spacing = 8 * devicePixelRatio
const watermarkSize = iconCanvas.width + spacing + metrics.width
context.fillText(
text,
canvas.width / 2 - watermarkSize / 2 + iconCanvas.width + spacing,
canvas.height - (canvas.height - size.height) / 4,
)
context.restore()
// Watermark icon
context.save()
context.drawImage(
iconCanvas,
canvas.width / 2 - watermarkSize / 2,
canvas.height - (canvas.height - size.height) / 4 - iconCanvas.height / 2,
)
context.restore()
clipboard.writeImage(nativeImage.createFromDataURL(canvas.toDataURL('png')))
feedbacking = true
}
}
</script>

<template>
<div
v-bind="$attrs"
class="camera-anchor"
@click="capture"
>
<VisualIcon v-if="feedbacking" name="lucide-clipboard-check" />
<VisualIcon v-else name="lucide-camera" />
</div>
</template>
12 changes: 12 additions & 0 deletions addons/camera/src/renderer/assets/icon-stroked.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
8 changes: 8 additions & 0 deletions addons/camera/src/renderer/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import * as commas from 'commas:api/renderer'
import CameraAnchor from './CameraAnchor.vue'

export default () => {

commas.context.provide('terminal.ui-right-action-anchor', CameraAnchor)

}
4 changes: 2 additions & 2 deletions resources/settings.spec.json
Original file line number Diff line number Diff line change
Expand Up @@ -445,7 +445,7 @@
"type": "string"
}
},
"recommendations": ["addon-manager", "ai", "cleaner", "cli", "clippy", "editor", "git", "iterm2", "l10n-ext", "launcher", "power-mode", "preference", "proxy", "settings", "sync", "theme", "updater"],
"default": ["addon-manager", "ai", "cleaner", "cli", "editor", "git", "iterm2", "l10n-ext", "launcher", "power-mode", "preference", "proxy", "settings", "sync", "theme", "updater"]
"recommendations": ["addon-manager", "ai", "camera", "cleaner", "cli", "clippy", "editor", "git", "iterm2", "l10n-ext", "launcher", "power-mode", "preference", "proxy", "settings", "sync", "theme", "updater"],
"default": ["addon-manager", "ai", "camera", "cleaner", "cli", "editor", "git", "iterm2", "l10n-ext", "launcher", "power-mode", "preference", "proxy", "settings", "sync", "theme", "updater"]
}
]

0 comments on commit c74721f

Please sign in to comment.