This commit is contained in:
@@ -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(
|
||||
|
||||
Binary file not shown.
@@ -3,6 +3,7 @@
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<meta name="color-scheme" content="light"/>
|
||||
<link rel="icon" type="image/jpeg" href="/src/assets/logo.jpeg"/>
|
||||
<title>Simple Care — Rikkei Education</title>
|
||||
</head>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&family=Share+Tech+Mono&display=swap');
|
||||
|
||||
:root {
|
||||
color-scheme: light only;
|
||||
--bg-main: #f5f7fa;
|
||||
--bg-card: #ffffff;
|
||||
--bg-subtle: #f0f3f7;
|
||||
@@ -36,6 +37,7 @@ html {
|
||||
height: 100%;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
color-scheme: light only;
|
||||
}
|
||||
|
||||
body {
|
||||
|
||||
@@ -51,6 +51,7 @@ let bannerStaffId = 0;
|
||||
// Quản lý webcam
|
||||
let webcamStream = null;
|
||||
let webcamInterval = null;
|
||||
let currentWebcamIntervalMs = null;
|
||||
const webcamVideo = document.createElement('video');
|
||||
webcamVideo.autoplay = true;
|
||||
webcamVideo.playsInline = true;
|
||||
@@ -884,8 +885,17 @@ function startStatsTicker() {
|
||||
setInterval(pullStats, 1000);
|
||||
}
|
||||
|
||||
async function startWebcam() {
|
||||
if (webcamStream) return;
|
||||
async function startWebcam(intervalMs) {
|
||||
if (!intervalMs || intervalMs <= 0) {
|
||||
intervalMs = 3000;
|
||||
}
|
||||
if (webcamStream) {
|
||||
if (currentWebcamIntervalMs === intervalMs) {
|
||||
return;
|
||||
}
|
||||
stopWebcam();
|
||||
}
|
||||
currentWebcamIntervalMs = intervalMs;
|
||||
try {
|
||||
webcamStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 320, height: 240, frameRate: { max: 10 } }
|
||||
@@ -898,7 +908,7 @@ async function startWebcam() {
|
||||
const dataUrl = webcamCanvas.toDataURL('image/jpeg', 0.4);
|
||||
window.go.main.App.SendWebcamFrame(dataUrl);
|
||||
}
|
||||
}, 250);
|
||||
}, intervalMs);
|
||||
} catch (err) {
|
||||
console.error('Failed to open webcam:', err);
|
||||
}
|
||||
@@ -909,6 +919,7 @@ function stopWebcam() {
|
||||
clearInterval(webcamInterval);
|
||||
webcamInterval = null;
|
||||
}
|
||||
currentWebcamIntervalMs = null;
|
||||
if (webcamStream) {
|
||||
webcamStream.getTracks().forEach(track => track.stop());
|
||||
webcamStream = null;
|
||||
|
||||
@@ -69,13 +69,14 @@ func main() {
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
BackgroundColour: &options.RGBA{R: 245, G: 247, B: 250, A: 1},
|
||||
OnStartup: app.startup,
|
||||
OnBeforeClose: func(ctx context.Context) (prevent bool) {
|
||||
return app.HandleBeforeClose()
|
||||
},
|
||||
Windows: &windows.Options{
|
||||
WebviewUserDataPath: app.webviewDataPath,
|
||||
Theme: windows.Light,
|
||||
},
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
|
||||
Reference in New Issue
Block a user