thu nghiem
All checks were successful
Deploy on Master Change / deploy (push) Successful in 1m21s

This commit is contained in:
2026-07-13 14:50:57 +07:00
parent 24a4ef2c66
commit 5ab5ed1454
9 changed files with 187 additions and 68 deletions

View File

@@ -142,8 +142,10 @@ type App struct {
unsyncedOff int
lastSyncTime time.Time
isStreamingSc bool
scIntervalMs int
streamScStop chan struct{}
isStreamingCam bool
camIntervalMs int
streamCamStop chan struct{}
statusMsg string
expectingLogin bool
@@ -229,8 +231,16 @@ func NewApp() *App {
}
}
func (a *App) forceLightTheme() {
if a.ctx == nil {
return
}
runtime.WindowSetLightTheme(a.ctx)
}
func (a *App) startup(ctx context.Context) {
a.ctx = ctx
a.forceLightTheme()
a.loadSession()
a.loadStats()
@@ -644,6 +654,7 @@ func (a *App) startLocalServer() {
a.mu.Lock()
a.localBrowserActive = true
a.mu.Unlock()
a.forceLightTheme()
go func() {
guard.SuppressFor(3 * time.Second)
runtime.WindowExecJS(a.ctx, fmt.Sprintf("window.location.href = %q", raw))
@@ -662,8 +673,10 @@ const examViewHTML = `<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>%s</title>
<style>
:root,html{color-scheme:light only}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-shrink:0}
@@ -741,8 +754,10 @@ const localBrowserHTML = `<!DOCTYPE html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="color-scheme" content="light">
<title>Trình duyệt Local — Simple Care</title>
<style>
:root,html{color-scheme:light only}
*{box-sizing:border-box;margin:0;padding:0}
body{font-family:Segoe UI,system-ui,sans-serif;background:#f5f7fa;color:#1a2332;min-height:100vh;display:flex;flex-direction:column}
.bar{display:flex;align-items:center;gap:8px;padding:10px 14px;background:#fff;border-bottom:1px solid #e4e9f0;flex-wrap:wrap}
@@ -1640,11 +1655,23 @@ func (a *App) connectWS() {
switch msg.Event {
case "start_screenshot_stream":
a.startScreenshotStream()
var intervalMs int
if val, ok := msg.Data["interval"]; ok {
if f, ok := val.(float64); ok {
intervalMs = int(f)
}
}
a.startScreenshotStream(intervalMs)
case "stop_screenshot_stream":
a.stopScreenshotStream()
case "start_webcam_stream":
a.startWebcamStream()
var intervalMs int
if val, ok := msg.Data["interval"]; ok {
if f, ok := val.(float64); ok {
intervalMs = int(f)
}
}
a.startWebcamStream(intervalMs)
case "stop_webcam_stream":
a.stopWebcamStream()
case "chat:message":
@@ -1707,18 +1734,28 @@ func (a *App) disconnectWS(expectedConn ...*websocket.Conn) {
}
}
func (a *App) startScreenshotStream() {
func (a *App) startScreenshotStream(intervalMs int) {
if intervalMs <= 0 {
intervalMs = 3000
}
a.mu.Lock()
if a.isStreamingSc {
a.mu.Unlock()
return
if a.scIntervalMs != intervalMs {
a.mu.Unlock()
a.stopScreenshotStream()
a.mu.Lock()
} else {
a.mu.Unlock()
return
}
}
a.isStreamingSc = true
a.scIntervalMs = intervalMs
a.streamScStop = make(chan struct{})
a.mu.Unlock()
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
defer ticker.Stop()
for {
@@ -1740,7 +1777,7 @@ func (a *App) startScreenshotStream() {
}
}
}()
log.Println("[WS] Screenshot screen-streaming started.")
log.Printf("[WS] Screenshot screen-streaming started with interval %dms.", intervalMs)
}
func (a *App) stopScreenshotStream() {
@@ -1751,23 +1788,34 @@ func (a *App) stopScreenshotStream() {
return
}
a.isStreamingSc = false
a.scIntervalMs = 0
close(a.streamScStop)
log.Println("[WS] Screenshot screen-streaming stopped.")
}
func (a *App) startWebcamStream() {
func (a *App) startWebcamStream(intervalMs int) {
if intervalMs <= 0 {
intervalMs = 3000
}
a.mu.Lock()
if a.isStreamingCam {
a.mu.Unlock()
return
if a.camIntervalMs != intervalMs {
a.mu.Unlock()
a.stopWebcamStream()
a.mu.Lock()
} else {
a.mu.Unlock()
return
}
}
a.isStreamingCam = true
a.camIntervalMs = intervalMs
a.streamCamStop = make(chan struct{})
a.mu.Unlock()
if !camera.IsNative {
log.Println("[WS] Platform is non-darwin, delegating webcam stream to frontend events")
runtime.EventsEmit(a.ctx, "start_webcam_stream")
log.Printf("[WS] Platform is non-darwin, delegating webcam stream to frontend events with interval %dms", intervalMs)
runtime.EventsEmit(a.ctx, "start_webcam_stream", intervalMs)
return
}
@@ -1780,7 +1828,7 @@ func (a *App) startWebcamStream() {
}
go func() {
ticker := time.NewTicker(250 * time.Millisecond)
ticker := time.NewTicker(time.Duration(intervalMs) * time.Millisecond)
defer ticker.Stop()
emptyCount := 0
sentCount := 0
@@ -1813,7 +1861,7 @@ func (a *App) startWebcamStream() {
}
}
}()
log.Println("[WS] Webcam native streaming started.")
log.Printf("[WS] Webcam native streaming started with interval %dms.", intervalMs)
}
func (a *App) stopWebcamStream() {
@@ -1824,6 +1872,7 @@ func (a *App) stopWebcamStream() {
return
}
a.isStreamingCam = false
a.camIntervalMs = 0
if a.streamCamStop != nil {
close(a.streamCamStop)
}
@@ -1980,6 +2029,7 @@ func (a *App) examStudentContext() (int64, *StudentExamSnapshot, error) {
// ReturnToDashboard quay lại giao diện chính của app (giữ cookie WebView2).
func (a *App) ReturnToDashboard() {
a.setLocalBrowserActive(false)
a.forceLightTheme()
guard.SuppressFor(5 * time.Second)
runtime.WindowReloadApp(a.ctx)
}
@@ -2057,6 +2107,7 @@ func (a *App) OpenGoogleTranslate() {
// OpenLocalBrowser mở trình duyệt chỉ cho phép localhost (test bài làm local).
func (a *App) OpenLocalBrowser() {
a.setLocalBrowserActive(true)
a.forceLightTheme()
guard.SuppressFor(5 * time.Second)
runtime.WindowExecJS(a.ctx, `window.location.href = 'http://127.0.0.1:34115/local-browser'`)
}
@@ -2142,6 +2193,7 @@ func (a *App) openExamWebView(targetURL, title string) error {
return errors.New("không có nội dung để mở")
}
a.setLocalBrowserActive(false)
a.forceLightTheme()
// URL local (PDF/resource): khung exam-view. URL ngoài (quiz): mở top-level giữ first-party cookies/session.
if isLocalExamURL(targetURL) {
wrapper := fmt.Sprintf(