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 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
+ };
@@ -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
+ }