Compare commits

..

2 Commits

Author SHA1 Message Date
0ff025fb15 fix block ci cd
Some checks failed
Deploy on Master Change / deploy (push) Has been cancelled
2026-07-13 15:52:35 +07:00
b56c6d722c fix tuong phan 2026-07-13 15:30:56 +07:00
15 changed files with 214 additions and 18 deletions

View File

@@ -46,13 +46,13 @@ cd client
chmod +x build.sh chmod +x build.sh
./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) ## 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:** * **Ubuntu / Debian / Linux Mint:**
```bash ```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: ### Lệnh chạy ứng dụng:
Cấp quyền chạy cho file và khởi chạy: Cấp quyền chạy cho file và khởi chạy:
```bash ```bash
chmod +x simple_care_v1.1 chmod +x simple_care_v1.2
./simple_care_v1.1 ./simple_care_v1.2
``` ```
--- ---

View File

@@ -306,10 +306,28 @@ func (a *App) HandleBeforeClose() (prevent bool) {
return false 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.reportViolation("app_closed", "Sinh viên tự đóng ứng dụng khi đang giám sát ("+mode+")")
a.tearDownBeforeQuit() a.tearDownBeforeQuit()
a.clearRunLock() a.clearRunLock()
return false 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). // 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).

View File

@@ -1,12 +1,12 @@
{ {
"name": "frontend", "name": "frontend",
"version": "1.1.0", "version": "1.2.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "frontend", "name": "frontend",
"version": "1.1.0", "version": "1.2.0",
"dependencies": { "dependencies": {
"pdfjs-dist": "^3.11.174" "pdfjs-dist": "^3.11.174"
}, },

View File

@@ -1,7 +1,7 @@
{ {
"name": "frontend", "name": "frontend",
"private": true, "private": true,
"version": "1.1.0", "version": "1.2.0",
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "vite build", "build": "vite build",

View File

@@ -1 +1 @@
39a3c87a78e46d8e7ef5f5042d044ee3 06a16f2c007333cdf273a92d4914c63b

View File

@@ -19,7 +19,7 @@ if (typeof window.go.main.App === 'undefined') {
} }
const brandLogoHtml = `<img src="${logoUrl}" alt="Simple Care" class="brand-logo-img" />`; 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ộ // Trạng thái cục bộ
let loggedIn = false; let loggedIn = false;

View File

@@ -172,6 +172,7 @@ var systemAllowed = map[string]bool{
"client": true, "client": true,
"simple_care_v1.0": true, "simple_care_v1.0": true,
"simple_care_v1.1": true, "simple_care_v1.1": true,
"simple_care_v1.2": true,
"simple_care": true, "simple_care": true,
"wails": true, "wails": true,
"code": true, // VSCode "code": true, // VSCode

View File

@@ -237,6 +237,7 @@ var systemAllowed = map[string]bool{
"client": true, "client": true,
"simple_care_v1.0": true, "simple_care_v1.0": true,
"simple_care_v1.1": true, "simple_care_v1.1": true,
"simple_care_v1.2": true,
"simple_care": true, "simple_care": true,
} }

View File

@@ -330,6 +330,7 @@ var systemAllowed = map[string]bool{
"client.exe": true, "client.exe": true,
"simple_care_v1.0.exe": true, "simple_care_v1.0.exe": true,
"simple_care_v1.1.exe": true, "simple_care_v1.1.exe": true,
"simple_care_v1.2.exe": true,
"simple_care.exe": true, "simple_care.exe": true,
"wails.exe": true, "wails.exe": true,
"msedgewebview2.exe": true, // WebView2 runtime (Wails renderer) "msedgewebview2.exe": true, // WebView2 runtime (Wails renderer)

View File

@@ -47,8 +47,53 @@ static unsigned char* CaptureFullDesktop(int* outLen, int quality) {
} }
@autoreleasepool { @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); 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) { if (!rep) {
*outLen = 0; *outLen = 0;

View File

@@ -6,11 +6,35 @@ import (
"bytes" "bytes"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"image"
"image/jpeg" "image/jpeg"
"github.com/kbinani/screenshot" "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,..." // 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 // Non-darwin: dùng kbinani/screenshot
func CaptureScreen() (string, error) { func CaptureScreen() (string, error) {
@@ -26,9 +50,36 @@ func CaptureScreen() (string, error) {
return "", fmt.Errorf("failed to capture screen: %w", err) 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 var buf bytes.Buffer
// Nén chất lượng JPEG khoảng 50% để truyền tải mượt mà qua mạng // 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 { if err != nil {
return "", fmt.Errorf("failed to encode jpeg: %w", err) return "", fmt.Errorf("failed to encode jpeg: %w", err)
} }

View File

@@ -62,7 +62,7 @@ func main() {
// Create application with options // Create application with options
err := wails.Run(&options.App{ err := wails.Run(&options.App{
Title: "Simple Care v1.1 — Rikkei Education", Title: "Simple Care v1.2 — Rikkei Education",
Width: 1024, Width: 1024,
Height: 768, Height: 768,
Menu: appMenu, Menu: appMenu,

View File

@@ -1,7 +1,7 @@
{ {
"$schema": "https://wails.io/schemas/config.v2.json", "$schema": "https://wails.io/schemas/config.v2.json",
"name": "Simple Care by Rikkei Edu", "name": "Simple Care by Rikkei Edu",
"outputfilename": "simple_care_v1.1", "outputfilename": "simple_care_v1.2",
"frontend:install": "npm install", "frontend:install": "npm install",
"frontend:build": "npm run build", "frontend:build": "npm run build",
"frontend:dev:watcher": "npm run dev", "frontend:dev:watcher": "npm run dev",

View File

@@ -254,7 +254,7 @@ export const ExamGridProctor: React.FC<ExamGridProctorProps> = ({
}; };
return ( return (
<div className="grid-proctor-container" ref={gridContainerRef}> <div className={`grid-proctor-container ${isFullscreen ? 'fullscreen-active' : ''}`} ref={gridContainerRef}>
<div className="grid-proctor-toolbar"> <div className="grid-proctor-toolbar">
<div className="grid-proctor-toolbar-left"> <div className="grid-proctor-toolbar-left">
<div className="search-input-wrapper"> <div className="search-input-wrapper">

View File

@@ -6100,3 +6100,82 @@ input:checked + .slider:before {
} }
} }
/* Fullscreen theme overrides */
.grid-proctor-container.fullscreen-active,
.grid-proctor-container:fullscreen {
padding: 2rem !important;
background: #121214 !important;
color: #e4e4eb !important;
}
.grid-proctor-container.fullscreen-active .grid-proctor-toolbar,
.grid-proctor-container:fullscreen .grid-proctor-toolbar {
background: #121214 !important;
border-bottom: 1px solid #2e2e38 !important;
top: -2rem !important;
padding-top: 2rem !important;
margin-top: -2rem !important;
}
.grid-proctor-container.fullscreen-active .checkbox-label,
.grid-proctor-container:fullscreen .checkbox-label {
color: #e4e4eb !important;
}
.grid-proctor-container.fullscreen-active .grid-cols-selector span,
.grid-proctor-container:fullscreen .grid-cols-selector span {
color: #a0a0b0 !important;
}
.grid-proctor-container.fullscreen-active select,
.grid-proctor-container:fullscreen select {
background-color: #1e1e24 !important;
color: #e4e4eb !important;
border-color: #2e2e38 !important;
}
.grid-proctor-container.fullscreen-active .grid-proctor-card,
.grid-proctor-container:fullscreen .grid-proctor-card {
background: #1e1e24 !important;
border-color: #2e2e38 !important;
}
.grid-proctor-container.fullscreen-active .grid-proctor-card-header,
.grid-proctor-container:fullscreen .grid-proctor-card-header {
background: #242b3b !important;
border-bottom: 1px solid #2e2e38 !important;
color: #e4e4eb !important;
}
.grid-proctor-container.fullscreen-active .student-name,
.grid-proctor-container:fullscreen .student-name {
color: #e4e4eb !important;
}
.grid-proctor-container.fullscreen-active .student-code,
.grid-proctor-container:fullscreen .student-code {
color: #ff7875 !important;
}
.grid-proctor-container.fullscreen-active .grid-proctor-card-footer,
.grid-proctor-container:fullscreen .grid-proctor-card-footer {
background: #242b3b !important;
border-top: 1px solid #2e2e38 !important;
color: #a0a0b0 !important;
}
.grid-proctor-container.fullscreen-active .assigned-paper,
.grid-proctor-container:fullscreen .assigned-paper {
color: #d9d9d9 !important;
}
.grid-proctor-container.fullscreen-active .grid-proctor-pagination,
.grid-proctor-container:fullscreen .grid-proctor-pagination {
border-top: 1px solid #2e2e38 !important;
}
.grid-proctor-container.fullscreen-active .grid-proctor-pagination span,
.grid-proctor-container:fullscreen .grid-proctor-pagination span {
color: #e4e4eb !important;
}