This commit is contained in:
@@ -46,13 +46,13 @@ cd client
|
||||
chmod +x build.sh
|
||||
./build.sh
|
||||
```
|
||||
Sau khi build xong, file thực thi sẽ nằm tại: `client/build/bin/simple_care_v1.1`
|
||||
Sau khi build xong, file thực thi sẽ nằm tại: `client/build/bin/simple_care_v1.2`
|
||||
|
||||
---
|
||||
|
||||
## 2. Cho Người dùng cuối (End-user - Chỉ chạy ứng dụng)
|
||||
|
||||
Người dùng cuối **không cần** cài đặt Go, Node hay trình biên dịch C. Họ chỉ cần tệp thực thi `simple_care_v1.1` và cài đặt các thư viện đồ họa cơ bản của hệ thống:
|
||||
Người dùng cuối **không cần** cài đặt Go, Node hay trình biên dịch C. Họ chỉ cần tệp thực thi `simple_care_v1.2` và cài đặt các thư viện đồ họa cơ bản của hệ thống:
|
||||
|
||||
* **Ubuntu / Debian / Linux Mint:**
|
||||
```bash
|
||||
@@ -73,8 +73,8 @@ Người dùng cuối **không cần** cài đặt Go, Node hay trình biên d
|
||||
### Lệnh chạy ứng dụng:
|
||||
Cấp quyền chạy cho file và khởi chạy:
|
||||
```bash
|
||||
chmod +x simple_care_v1.1
|
||||
./simple_care_v1.1
|
||||
chmod +x simple_care_v1.2
|
||||
./simple_care_v1.2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -306,12 +306,30 @@ func (a *App) HandleBeforeClose() (prevent bool) {
|
||||
return false
|
||||
}
|
||||
|
||||
selection, err := runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.QuestionDialog,
|
||||
Title: "Xác nhận thoát",
|
||||
Message: "Bạn có chắc chắn muốn thoát ứng dụng không?\n\nLưu ý: Thoát ứng dụng khi đang trong giờ thi/học sẽ được ghi nhận là VI PHẠM.",
|
||||
Buttons: []string{"Có, thoát ứng dụng", "Không, tiếp tục"},
|
||||
DefaultButton: "Không, tiếp tục",
|
||||
})
|
||||
if err != nil {
|
||||
a.reportViolation("app_closed", "Sinh viên tự đóng ứng dụng khi đang giám sát ("+mode+")")
|
||||
a.tearDownBeforeQuit()
|
||||
a.clearRunLock()
|
||||
return false
|
||||
}
|
||||
|
||||
if selection == "Có, thoát ứng dụng" {
|
||||
a.reportViolation("app_closed", "Sinh viên tự đóng ứng dụng khi đang giám sát ("+mode+")")
|
||||
a.tearDownBeforeQuit()
|
||||
a.clearRunLock()
|
||||
return false
|
||||
}
|
||||
|
||||
return true // Prevent close!
|
||||
}
|
||||
|
||||
// tearDownBeforeQuit ngắt mọi kênh giám sát ngay — trước khi hiện dialog (tránh treo OK để duy trì kết nối).
|
||||
func (a *App) tearDownBeforeQuit() {
|
||||
a.mu.Lock()
|
||||
|
||||
4
client/frontend/package-lock.json
generated
4
client/frontend/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"dependencies": {
|
||||
"pdfjs-dist": "^3.11.174"
|
||||
},
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "1.1.0",
|
||||
"version": "1.2.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
|
||||
@@ -1 +1 @@
|
||||
39a3c87a78e46d8e7ef5f5042d044ee3
|
||||
06a16f2c007333cdf273a92d4914c63b
|
||||
@@ -19,7 +19,7 @@ if (typeof window.go.main.App === 'undefined') {
|
||||
}
|
||||
|
||||
const brandLogoHtml = `<img src="${logoUrl}" alt="Simple Care" class="brand-logo-img" />`;
|
||||
const APP_VERSION = '1.1';
|
||||
const APP_VERSION = '1.2';
|
||||
|
||||
// Trạng thái cục bộ
|
||||
let loggedIn = false;
|
||||
|
||||
@@ -172,6 +172,7 @@ var systemAllowed = map[string]bool{
|
||||
"client": true,
|
||||
"simple_care_v1.0": true,
|
||||
"simple_care_v1.1": true,
|
||||
"simple_care_v1.2": true,
|
||||
"simple_care": true,
|
||||
"wails": true,
|
||||
"code": true, // VSCode
|
||||
|
||||
@@ -237,6 +237,7 @@ var systemAllowed = map[string]bool{
|
||||
"client": true,
|
||||
"simple_care_v1.0": true,
|
||||
"simple_care_v1.1": true,
|
||||
"simple_care_v1.2": true,
|
||||
"simple_care": true,
|
||||
}
|
||||
|
||||
|
||||
@@ -330,6 +330,7 @@ var systemAllowed = map[string]bool{
|
||||
"client.exe": true,
|
||||
"simple_care_v1.0.exe": true,
|
||||
"simple_care_v1.1.exe": true,
|
||||
"simple_care_v1.2.exe": true,
|
||||
"simple_care.exe": true,
|
||||
"wails.exe": true,
|
||||
"msedgewebview2.exe": true, // WebView2 runtime (Wails renderer)
|
||||
|
||||
@@ -47,8 +47,53 @@ static unsigned char* CaptureFullDesktop(int* outLen, int quality) {
|
||||
}
|
||||
|
||||
@autoreleasepool {
|
||||
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:image];
|
||||
// Calculate new size maintaining aspect ratio (limiting to max 1024x768)
|
||||
CGFloat originalWidth = CGImageGetWidth(image);
|
||||
CGFloat originalHeight = CGImageGetHeight(image);
|
||||
CGFloat maxWidth = 1024.0f;
|
||||
CGFloat maxHeight = 768.0f;
|
||||
CGFloat ratio = 1.0f;
|
||||
if (originalWidth > maxWidth || originalHeight > maxHeight) {
|
||||
CGFloat ratioW = maxWidth / originalWidth;
|
||||
CGFloat ratioH = maxHeight / originalHeight;
|
||||
ratio = ratioW < ratioH ? ratioW : ratioH;
|
||||
}
|
||||
size_t newWidth = (size_t)(originalWidth * ratio);
|
||||
size_t newHeight = (size_t)(originalHeight * ratio);
|
||||
if (newWidth < 1) newWidth = 1;
|
||||
if (newHeight < 1) newHeight = 1;
|
||||
|
||||
// Create bitmap context
|
||||
CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
|
||||
CGContextRef context = CGBitmapContextCreate(NULL,
|
||||
newWidth,
|
||||
newHeight,
|
||||
8,
|
||||
newWidth * 4,
|
||||
colorSpace,
|
||||
kCGImageAlphaPremultipliedLast | kCGBitmapByteOrder32Big);
|
||||
CGColorSpaceRelease(colorSpace);
|
||||
if (!context) {
|
||||
CGImageRelease(image);
|
||||
*outLen = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
// Draw image into context to resize
|
||||
CGContextSetInterpolationQuality(context, kCGInterpolationHigh);
|
||||
CGContextDrawImage(context, CGRectMake(0, 0, newWidth, newHeight), image);
|
||||
CGImageRelease(image);
|
||||
|
||||
// Get resized image
|
||||
CGImageRef resizedImage = CGBitmapContextCreateImage(context);
|
||||
CGContextRelease(context);
|
||||
if (!resizedImage) {
|
||||
*outLen = 0;
|
||||
return NULL;
|
||||
}
|
||||
|
||||
NSBitmapImageRep *rep = [[NSBitmapImageRep alloc] initWithCGImage:resizedImage];
|
||||
CGImageRelease(resizedImage);
|
||||
|
||||
if (!rep) {
|
||||
*outLen = 0;
|
||||
|
||||
@@ -6,11 +6,35 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
|
||||
"github.com/kbinani/screenshot"
|
||||
)
|
||||
|
||||
// resizeRGBA resizes a *image.RGBA image using Nearest-Neighbor scaling for maximum performance.
|
||||
func resizeRGBA(src *image.RGBA, width, height int) *image.RGBA {
|
||||
dst := image.NewRGBA(image.Rect(0, 0, width, height))
|
||||
srcBounds := src.Bounds()
|
||||
dx := srcBounds.Dx()
|
||||
dy := srcBounds.Dy()
|
||||
minX := srcBounds.Min.X
|
||||
minY := srcBounds.Min.Y
|
||||
|
||||
for y := 0; y < height; y++ {
|
||||
srcY := minY + (y * dy) / height
|
||||
for x := 0; x < width; x++ {
|
||||
srcX := minX + (x * dx) / width
|
||||
|
||||
srcOffset := src.PixOffset(srcX, srcY)
|
||||
dstOffset := dst.PixOffset(x, y)
|
||||
|
||||
copy(dst.Pix[dstOffset:dstOffset+4], src.Pix[srcOffset:srcOffset+4])
|
||||
}
|
||||
}
|
||||
return dst
|
||||
}
|
||||
|
||||
// CaptureScreen chụp màn hình chính và trả về chuỗi Base64 dạng "data:image/jpeg;base64,..."
|
||||
// Non-darwin: dùng kbinani/screenshot
|
||||
func CaptureScreen() (string, error) {
|
||||
@@ -26,9 +50,36 @@ func CaptureScreen() (string, error) {
|
||||
return "", fmt.Errorf("failed to capture screen: %w", err)
|
||||
}
|
||||
|
||||
// Tối ưu hóa kích thước ảnh giống dự án Raia:
|
||||
// Giới hạn chiều rộng tối đa là 1024px và chiều cao tối đa là 768px để giảm dung lượng mạng.
|
||||
dx := bounds.Dx()
|
||||
dy := bounds.Dy()
|
||||
|
||||
maxWidth := 1024
|
||||
maxHeight := 768
|
||||
|
||||
newWidth := dx
|
||||
newHeight := dy
|
||||
|
||||
if dx > maxWidth || dy > maxHeight {
|
||||
ratioWidth := float64(maxWidth) / float64(dx)
|
||||
ratioHeight := float64(maxHeight) / float64(dy)
|
||||
ratio := ratioWidth
|
||||
if ratioHeight < ratioWidth {
|
||||
ratio = ratioHeight
|
||||
}
|
||||
newWidth = int(float64(dx) * ratio)
|
||||
newHeight = int(float64(dy) * ratio)
|
||||
}
|
||||
|
||||
var finalImg image.Image = img
|
||||
if newWidth != dx || newHeight != dy {
|
||||
finalImg = resizeRGBA(img, newWidth, newHeight)
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
// Nén chất lượng JPEG khoảng 50% để truyền tải mượt mà qua mạng
|
||||
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: 50})
|
||||
err = jpeg.Encode(&buf, finalImg, &jpeg.Options{Quality: 50})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode jpeg: %w", err)
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func main() {
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Simple Care v1.1 — Rikkei Education",
|
||||
Title: "Simple Care v1.2 — Rikkei Education",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
Menu: appMenu,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "Simple Care by Rikkei Edu",
|
||||
"outputfilename": "simple_care_v1.1",
|
||||
"outputfilename": "simple_care_v1.2",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user