easyscreen-shot 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/bin/config.js +30 -0
- package/bin/easyshot.js +60 -0
- package/index.html +49 -0
- package/main.js +119 -0
- package/package.json +39 -0
- package/renderer.js +139 -0
- package/styles.css +154 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 JASON-QWeb
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# EasyScreenShot (EasyShot)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
## 核心优势
|
|
5
|
+
|
|
6
|
+
- 支持输入截图尺寸
|
|
7
|
+
- 支持连续截图
|
|
8
|
+
- 支持边调整后方内容边截图
|
|
9
|
+
|
|
10
|
+
## 安装
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
npm install -g .
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## 使用方法
|
|
17
|
+
|
|
18
|
+
### 基础截图
|
|
19
|
+
```bash
|
|
20
|
+
easyshot
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### 连续截图 (-w)
|
|
24
|
+
截图后不退出,保持取景框位置,适合连续截取同一位置的内容
|
|
25
|
+
```bash
|
|
26
|
+
easyshot -w
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
### 自定义保存路径 (-o)
|
|
30
|
+
将截图保存到指定文件夹
|
|
31
|
+
```bash
|
|
32
|
+
easyshot -o ~/Documents
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### 查看帮助 (-h)
|
|
36
|
+
```bash
|
|
37
|
+
easyshot -h
|
|
38
|
+
```
|
package/bin/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
|
|
5
|
+
const CONFIG_PATH = path.join(os.homedir(), '.easyshot-config.json');
|
|
6
|
+
|
|
7
|
+
function loadConfig() {
|
|
8
|
+
try {
|
|
9
|
+
if (fs.existsSync(CONFIG_PATH)) {
|
|
10
|
+
const data = fs.readFileSync(CONFIG_PATH, 'utf-8');
|
|
11
|
+
return JSON.parse(data);
|
|
12
|
+
}
|
|
13
|
+
} catch (error) {
|
|
14
|
+
console.error('Error loading config:', error.message);
|
|
15
|
+
}
|
|
16
|
+
return {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function saveConfig(config) {
|
|
20
|
+
try {
|
|
21
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), 'utf-8');
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('Error saving config:', error.message);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports = {
|
|
28
|
+
loadConfig,
|
|
29
|
+
saveConfig
|
|
30
|
+
};
|
package/bin/easyshot.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const { program } = require('commander');
|
|
4
|
+
const { spawn } = require('child_process');
|
|
5
|
+
const electron = require('electron');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
|
|
9
|
+
const { loadConfig, saveConfig } = require('./config');
|
|
10
|
+
|
|
11
|
+
program
|
|
12
|
+
.name('easyshot')
|
|
13
|
+
.description('EasyScreenShot CLI Tool')
|
|
14
|
+
.version('1.0.0', '-v, --version', 'output the version number')
|
|
15
|
+
.option('-w, --watch', 'Watch mode: Consistent capture (Do not exit after capture)', false)
|
|
16
|
+
.option('-o, --output <dir>', 'Set default output directory for screenshots');
|
|
17
|
+
|
|
18
|
+
program.parse(process.argv);
|
|
19
|
+
|
|
20
|
+
const options = program.opts();
|
|
21
|
+
|
|
22
|
+
// Handle -o / --output as configuration setter
|
|
23
|
+
if (options.output) {
|
|
24
|
+
const absPath = path.resolve(process.cwd(), options.output);
|
|
25
|
+
if (!fs.existsSync(absPath)) {
|
|
26
|
+
console.error(`Error: Directory does not exist: ${absPath}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const config = loadConfig();
|
|
31
|
+
config.defaultOutput = absPath;
|
|
32
|
+
saveConfig(config);
|
|
33
|
+
|
|
34
|
+
console.log(`✅ Default output directory updated to: ${absPath}`);
|
|
35
|
+
process.exit(0);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Launch Application
|
|
39
|
+
const isWatch = options.watch;
|
|
40
|
+
const mainPath = path.join(__dirname, '..', 'main.js');
|
|
41
|
+
const appArgs = [];
|
|
42
|
+
|
|
43
|
+
if (isWatch) appArgs.push('--watch');
|
|
44
|
+
|
|
45
|
+
// Load default output from config
|
|
46
|
+
const config = loadConfig();
|
|
47
|
+
if (config.defaultOutput) {
|
|
48
|
+
// Verify it still exists
|
|
49
|
+
if (fs.existsSync(config.defaultOutput)) {
|
|
50
|
+
appArgs.push('--output', config.defaultOutput);
|
|
51
|
+
} else {
|
|
52
|
+
console.warn(`⚠️ Configured output directory not found (${config.defaultOutput}). Using default (Desktop).`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const child = spawn(electron, [mainPath, ...appArgs], { stdio: 'inherit' });
|
|
57
|
+
|
|
58
|
+
child.on('close', (code) => {
|
|
59
|
+
process.exit(code);
|
|
60
|
+
});
|
package/index.html
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html>
|
|
3
|
+
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="UTF-8">
|
|
6
|
+
<link rel="stylesheet" href="styles.css">
|
|
7
|
+
</head>
|
|
8
|
+
|
|
9
|
+
<body>
|
|
10
|
+
<!-- The selection box (View Finder) -->
|
|
11
|
+
<div id="selection-box">
|
|
12
|
+
<!-- Resize Handles -->
|
|
13
|
+
<div class="handle nw" data-dir="nw"></div>
|
|
14
|
+
<div class="handle n" data-dir="n"></div>
|
|
15
|
+
<div class="handle ne" data-dir="ne"></div>
|
|
16
|
+
<div class="handle e" data-dir="e"></div>
|
|
17
|
+
<div class="handle se" data-dir="se"></div>
|
|
18
|
+
<div class="handle s" data-dir="s"></div>
|
|
19
|
+
<div class="handle sw" data-dir="sw"></div>
|
|
20
|
+
<div class="handle w" data-dir="w"></div>
|
|
21
|
+
|
|
22
|
+
<!-- Toolbar -->
|
|
23
|
+
<div id="toolbar">
|
|
24
|
+
<div class="inputs">
|
|
25
|
+
<label>W: <input type="number" id="width-input"></label>
|
|
26
|
+
<label>H: <input type="number" id="height-input"></label>
|
|
27
|
+
</div>
|
|
28
|
+
<div class="actions">
|
|
29
|
+
<button id="close-btn" class="icon-btn cancel" title="Close">
|
|
30
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
|
31
|
+
stroke-linejoin="round">
|
|
32
|
+
<line x1="18" y1="6" x2="6" y2="18"></line>
|
|
33
|
+
<line x1="6" y1="6" x2="18" y2="18"></line>
|
|
34
|
+
</svg>
|
|
35
|
+
</button>
|
|
36
|
+
<button id="save-btn" class="icon-btn save" title="Save">
|
|
37
|
+
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round"
|
|
38
|
+
stroke-linejoin="round">
|
|
39
|
+
<polyline points="20 6 9 17 4 12"></polyline>
|
|
40
|
+
</svg>
|
|
41
|
+
</button>
|
|
42
|
+
</div>
|
|
43
|
+
</div>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<script src="renderer.js"></script>
|
|
47
|
+
</body>
|
|
48
|
+
|
|
49
|
+
</html>
|
package/main.js
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
const { app, BrowserWindow, ipcMain, desktopCapturer, screen } = require('electron');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const os = require('os');
|
|
5
|
+
|
|
6
|
+
let mainWindow;
|
|
7
|
+
|
|
8
|
+
// Parse args manually since Electron combines own args
|
|
9
|
+
// Structure is: electron_binary main.js --watch --output ...
|
|
10
|
+
const args = process.argv.slice(2);
|
|
11
|
+
const isWatch = args.includes('--watch');
|
|
12
|
+
let outputDir = path.join(os.homedir(), 'Desktop');
|
|
13
|
+
|
|
14
|
+
const outputIndex = args.indexOf('--output');
|
|
15
|
+
if (outputIndex !== -1 && args[outputIndex + 1]) {
|
|
16
|
+
outputDir = args[outputIndex + 1];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function createWindow() {
|
|
20
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
21
|
+
const { width, height } = primaryDisplay.bounds;
|
|
22
|
+
|
|
23
|
+
mainWindow = new BrowserWindow({
|
|
24
|
+
width,
|
|
25
|
+
height,
|
|
26
|
+
x: 0,
|
|
27
|
+
y: 0,
|
|
28
|
+
frame: false,
|
|
29
|
+
transparent: true,
|
|
30
|
+
alwaysOnTop: true,
|
|
31
|
+
skipTaskbar: true,
|
|
32
|
+
resizable: false,
|
|
33
|
+
movable: false,
|
|
34
|
+
hasShadow: false,
|
|
35
|
+
enableLargerThanScreen: true,
|
|
36
|
+
webPreferences: {
|
|
37
|
+
nodeIntegration: true,
|
|
38
|
+
contextIsolation: false,
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
mainWindow.loadFile('index.html');
|
|
43
|
+
mainWindow.setIgnoreMouseEvents(true, { forward: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
app.whenReady().then(() => {
|
|
47
|
+
createWindow();
|
|
48
|
+
|
|
49
|
+
app.on('activate', () => {
|
|
50
|
+
if (BrowserWindow.getAllWindows().length === 0) createWindow();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
app.on('window-all-closed', () => {
|
|
55
|
+
if (process.platform !== 'darwin') app.quit();
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
ipcMain.on('set-ignore-mouse-events', (event, ignore, options) => {
|
|
59
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
60
|
+
if (win) win.setIgnoreMouseEvents(ignore, options);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
ipcMain.on('close-app', () => {
|
|
64
|
+
app.quit();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
ipcMain.on('save-screenshot', async (event, rect) => {
|
|
68
|
+
const win = BrowserWindow.fromWebContents(event.sender);
|
|
69
|
+
win.hide(); // Hide before capturing
|
|
70
|
+
|
|
71
|
+
setTimeout(async () => {
|
|
72
|
+
try {
|
|
73
|
+
const primaryDisplay = screen.getPrimaryDisplay();
|
|
74
|
+
|
|
75
|
+
const sources = await desktopCapturer.getSources({
|
|
76
|
+
types: ['screen'],
|
|
77
|
+
thumbnailSize: {
|
|
78
|
+
width: primaryDisplay.size.width * primaryDisplay.scaleFactor,
|
|
79
|
+
height: primaryDisplay.size.height * primaryDisplay.scaleFactor
|
|
80
|
+
}
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const img = sources[0].thumbnail;
|
|
84
|
+
const scale = primaryDisplay.scaleFactor;
|
|
85
|
+
const cropRect = {
|
|
86
|
+
x: Math.round(rect.x * scale),
|
|
87
|
+
y: Math.round(rect.y * scale),
|
|
88
|
+
width: Math.round(rect.width * scale),
|
|
89
|
+
height: Math.round(rect.height * scale)
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const crop = img.crop(cropRect);
|
|
93
|
+
const pngBuffer = crop.toPNG();
|
|
94
|
+
|
|
95
|
+
const pad = (n) => n.toString().padStart(2, "0");
|
|
96
|
+
const now = new Date();
|
|
97
|
+
const timestamp = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}_${pad(now.getHours())}-${pad(now.getMinutes())}-${pad(now.getSeconds())}`;
|
|
98
|
+
const filename = `Screenshot_${timestamp}.png`;
|
|
99
|
+
const filePath = path.join(outputDir, filename);
|
|
100
|
+
|
|
101
|
+
fs.writeFile(filePath, pngBuffer, (err) => {
|
|
102
|
+
if (err) console.error('Failed to save:', err);
|
|
103
|
+
|
|
104
|
+
if (isWatch) {
|
|
105
|
+
// Restore window if watch mode
|
|
106
|
+
win.show();
|
|
107
|
+
console.log(`Saved to ${filePath}. Continuing...`);
|
|
108
|
+
} else {
|
|
109
|
+
console.log(`Saved to ${filePath}. Exiting.`);
|
|
110
|
+
app.quit();
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
} catch (e) {
|
|
115
|
+
console.error(e);
|
|
116
|
+
app.quit();
|
|
117
|
+
}
|
|
118
|
+
}, 100);
|
|
119
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "easyscreen-shot",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Cross-platform screenshot tool with Green Box UI",
|
|
5
|
+
"main": "main.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"easyshot": "bin/easyshot.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"start": "electron .",
|
|
11
|
+
"pack": "electron-builder --dir",
|
|
12
|
+
"dist": "electron-builder"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"screenshot",
|
|
16
|
+
"electron",
|
|
17
|
+
"screen-capture",
|
|
18
|
+
"mac",
|
|
19
|
+
"windows",
|
|
20
|
+
"linux"
|
|
21
|
+
],
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/JASON-QWeb/EasyScreenShot.git"
|
|
25
|
+
},
|
|
26
|
+
"author": "jasonqweb",
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"homepage": "https://github.com/JASON-QWeb/EasyScreenShot#readme",
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/JASON-QWeb/EasyScreenShot/issues"
|
|
31
|
+
},
|
|
32
|
+
"devDependencies": {
|
|
33
|
+
"electron": "^39.2.7",
|
|
34
|
+
"electron-builder": "^24.0.0"
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"commander": "^14.0.2"
|
|
38
|
+
}
|
|
39
|
+
}
|
package/renderer.js
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
const { ipcRenderer } = require('electron');
|
|
2
|
+
|
|
3
|
+
const selectionBox = document.getElementById('selection-box');
|
|
4
|
+
const widthInput = document.getElementById('width-input');
|
|
5
|
+
const heightInput = document.getElementById('height-input');
|
|
6
|
+
const closeBtn = document.getElementById('close-btn');
|
|
7
|
+
const saveBtn = document.getElementById('save-btn');
|
|
8
|
+
|
|
9
|
+
let isDragging = false;
|
|
10
|
+
let isResizing = false;
|
|
11
|
+
let resizeDir = '';
|
|
12
|
+
let startX, startY;
|
|
13
|
+
let initialRect = {};
|
|
14
|
+
|
|
15
|
+
// Initialize Default Box
|
|
16
|
+
function initBox() {
|
|
17
|
+
// Center a 600x400 box
|
|
18
|
+
const startW = 600;
|
|
19
|
+
const startH = 400;
|
|
20
|
+
const startL = (window.innerWidth - startW) / 2;
|
|
21
|
+
const startT = (window.innerHeight - startH) / 2;
|
|
22
|
+
|
|
23
|
+
updateSelection(startL, startT, startW, startH);
|
|
24
|
+
selectionBox.style.display = 'block';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Mouse Event Forwarding Logic
|
|
28
|
+
|
|
29
|
+
selectionBox.addEventListener('mouseenter', () => {
|
|
30
|
+
ipcRenderer.send('set-ignore-mouse-events', false);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
selectionBox.addEventListener('mouseleave', () => {
|
|
34
|
+
// Only ignore if we are NOT dragging/resizing
|
|
35
|
+
if (!isDragging && !isResizing) {
|
|
36
|
+
ipcRenderer.send('set-ignore-mouse-events', true, { forward: true });
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Dragging Logic
|
|
41
|
+
selectionBox.addEventListener('mousedown', (e) => {
|
|
42
|
+
if (e.target.classList.contains('handle')) {
|
|
43
|
+
isResizing = true;
|
|
44
|
+
resizeDir = e.target.dataset.dir;
|
|
45
|
+
startX = e.clientX;
|
|
46
|
+
startY = e.clientY;
|
|
47
|
+
initialRect = getRect();
|
|
48
|
+
} else if (e.target.closest('#toolbar')) {
|
|
49
|
+
// Toolbar interaction, naturally handled by buttons/inputs
|
|
50
|
+
// But we need to ensure we don't start dragging the box if clicking empty space in toolbar
|
|
51
|
+
e.stopPropagation();
|
|
52
|
+
} else {
|
|
53
|
+
// Dragging the box
|
|
54
|
+
isDragging = true;
|
|
55
|
+
startX = e.clientX;
|
|
56
|
+
startY = e.clientY;
|
|
57
|
+
initialRect = getRect();
|
|
58
|
+
document.body.style.cursor = 'move';
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
window.addEventListener('mousemove', (e) => {
|
|
63
|
+
if (isDragging) {
|
|
64
|
+
const dx = e.clientX - startX;
|
|
65
|
+
const dy = e.clientY - startY;
|
|
66
|
+
|
|
67
|
+
updateSelection(initialRect.left + dx, initialRect.top + dy, initialRect.width, initialRect.height);
|
|
68
|
+
} else if (isResizing) {
|
|
69
|
+
let { left, top, width, height } = initialRect;
|
|
70
|
+
const dx = e.clientX - startX;
|
|
71
|
+
const dy = e.clientY - startY;
|
|
72
|
+
|
|
73
|
+
if (resizeDir.includes('e')) width += dx;
|
|
74
|
+
if (resizeDir.includes('w')) { left += dx; width -= dx; }
|
|
75
|
+
if (resizeDir.includes('s')) height += dy;
|
|
76
|
+
if (resizeDir.includes('n')) { top += dy; height -= dy; }
|
|
77
|
+
|
|
78
|
+
if (width > 0 && height > 0) {
|
|
79
|
+
updateSelection(left, top, width, height);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
window.addEventListener('mouseup', () => {
|
|
85
|
+
isDragging = false;
|
|
86
|
+
isResizing = false;
|
|
87
|
+
document.body.style.cursor = 'default';
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
function updateSelection(left, top, w, h) {
|
|
91
|
+
selectionBox.style.left = left + 'px';
|
|
92
|
+
selectionBox.style.top = top + 'px';
|
|
93
|
+
selectionBox.style.width = w + 'px';
|
|
94
|
+
selectionBox.style.height = h + 'px';
|
|
95
|
+
|
|
96
|
+
widthInput.value = Math.round(w);
|
|
97
|
+
heightInput.value = Math.round(h);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getRect() {
|
|
101
|
+
return {
|
|
102
|
+
left: parseInt(selectionBox.style.left || 0),
|
|
103
|
+
top: parseInt(selectionBox.style.top || 0),
|
|
104
|
+
width: parseInt(selectionBox.style.width || 0),
|
|
105
|
+
height: parseInt(selectionBox.style.height || 0)
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Toolbar Inputs
|
|
110
|
+
widthInput.addEventListener('change', () => {
|
|
111
|
+
const w = parseInt(widthInput.value);
|
|
112
|
+
if (w > 0) selectionBox.style.width = w + 'px';
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
heightInput.addEventListener('change', () => {
|
|
116
|
+
const h = parseInt(heightInput.value);
|
|
117
|
+
if (h > 0) selectionBox.style.height = h + 'px';
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Buttons
|
|
121
|
+
closeBtn.addEventListener('click', () => {
|
|
122
|
+
ipcRenderer.send('close-app');
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
saveBtn.addEventListener('click', () => {
|
|
126
|
+
const rect = getRect();
|
|
127
|
+
// rect uses CSS pixels (dom coords).
|
|
128
|
+
// main process will handle scaling.
|
|
129
|
+
// Pass x, y, width, height.
|
|
130
|
+
ipcRenderer.send('save-screenshot', {
|
|
131
|
+
x: rect.left,
|
|
132
|
+
y: rect.top,
|
|
133
|
+
width: rect.width,
|
|
134
|
+
height: rect.height
|
|
135
|
+
});
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
// Run Init
|
|
139
|
+
initBox();
|
package/styles.css
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
body {
|
|
2
|
+
margin: 0;
|
|
3
|
+
padding: 0;
|
|
4
|
+
overflow: hidden;
|
|
5
|
+
background-color: transparent;
|
|
6
|
+
user-select: none;
|
|
7
|
+
font-family: sans-serif;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
#selection-box {
|
|
11
|
+
position: absolute;
|
|
12
|
+
z-index: 3;
|
|
13
|
+
border: 2px solid #00ff00;
|
|
14
|
+
/* Green border */
|
|
15
|
+
/* Remove box-shadow dimming since we want full visibility */
|
|
16
|
+
display: none;
|
|
17
|
+
/* Hidden until init */
|
|
18
|
+
cursor: move;
|
|
19
|
+
background: transparent;
|
|
20
|
+
/* Ensure center is transparent */
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/* Resize Handles */
|
|
24
|
+
.handle {
|
|
25
|
+
position: absolute;
|
|
26
|
+
width: 10px;
|
|
27
|
+
height: 10px;
|
|
28
|
+
background-color: #00ff00;
|
|
29
|
+
border-radius: 50%;
|
|
30
|
+
z-index: 4;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.handle.nw {
|
|
34
|
+
top: -6px;
|
|
35
|
+
left: -6px;
|
|
36
|
+
cursor: nw-resize;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
.handle.n {
|
|
40
|
+
top: -6px;
|
|
41
|
+
left: 50%;
|
|
42
|
+
margin-left: -5px;
|
|
43
|
+
cursor: n-resize;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.handle.ne {
|
|
47
|
+
top: -6px;
|
|
48
|
+
right: -6px;
|
|
49
|
+
cursor: ne-resize;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.handle.e {
|
|
53
|
+
top: 50%;
|
|
54
|
+
right: -6px;
|
|
55
|
+
margin-top: -5px;
|
|
56
|
+
cursor: e-resize;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.handle.se {
|
|
60
|
+
bottom: -6px;
|
|
61
|
+
right: -6px;
|
|
62
|
+
cursor: se-resize;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.handle.s {
|
|
66
|
+
bottom: -6px;
|
|
67
|
+
left: 50%;
|
|
68
|
+
margin-left: -5px;
|
|
69
|
+
cursor: s-resize;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
.handle.sw {
|
|
73
|
+
bottom: -6px;
|
|
74
|
+
left: -6px;
|
|
75
|
+
cursor: sw-resize;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.handle.w {
|
|
79
|
+
top: 50%;
|
|
80
|
+
left: -6px;
|
|
81
|
+
margin-top: -5px;
|
|
82
|
+
cursor: w-resize;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/* Toolbar */
|
|
86
|
+
#toolbar {
|
|
87
|
+
position: absolute;
|
|
88
|
+
bottom: 5px;
|
|
89
|
+
right: 5px;
|
|
90
|
+
background-color: rgba(240, 240, 240, 0.9);
|
|
91
|
+
padding: 5px;
|
|
92
|
+
border-radius: 4px;
|
|
93
|
+
display: flex;
|
|
94
|
+
gap: 10px;
|
|
95
|
+
align-items: center;
|
|
96
|
+
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
|
|
97
|
+
cursor: default;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.inputs {
|
|
101
|
+
display: flex;
|
|
102
|
+
gap: 5px;
|
|
103
|
+
font-size: 12px;
|
|
104
|
+
color: #333;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
.inputs input {
|
|
108
|
+
width: 40px;
|
|
109
|
+
padding: 2px;
|
|
110
|
+
border: 1px solid #ccc;
|
|
111
|
+
border-radius: 2px;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
.actions {
|
|
115
|
+
display: flex;
|
|
116
|
+
gap: 5px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.icon-btn {
|
|
120
|
+
background: white;
|
|
121
|
+
border: 1px solid #ddd;
|
|
122
|
+
width: 28px;
|
|
123
|
+
height: 28px;
|
|
124
|
+
border-radius: 4px;
|
|
125
|
+
display: flex;
|
|
126
|
+
align-items: center;
|
|
127
|
+
justify-content: center;
|
|
128
|
+
cursor: pointer;
|
|
129
|
+
transition: all 0.2s ease;
|
|
130
|
+
color: #555;
|
|
131
|
+
padding: 0;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
.icon-btn svg {
|
|
135
|
+
width: 18px;
|
|
136
|
+
height: 18px;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.icon-btn:hover {
|
|
140
|
+
transform: scale(1.05);
|
|
141
|
+
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
.icon-btn.cancel:hover {
|
|
145
|
+
background-color: #ffebee;
|
|
146
|
+
color: #d32f2f;
|
|
147
|
+
border-color: #ffcdd2;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.icon-btn.save:hover {
|
|
151
|
+
background-color: #e8f5e9;
|
|
152
|
+
color: #2e7d32;
|
|
153
|
+
border-color: #c8e6c9;
|
|
154
|
+
}
|