core v1
This commit is contained in:
3
client/.gitignore
vendored
Normal file
3
client/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
build/bin
|
||||
node_modules
|
||||
frontend/dist
|
||||
19
client/README.md
Normal file
19
client/README.md
Normal file
@@ -0,0 +1,19 @@
|
||||
# README
|
||||
|
||||
## About
|
||||
|
||||
This is the official Wails Vanilla template.
|
||||
|
||||
You can configure the project by editing `wails.json`. More information about the project settings can be found
|
||||
here: https://wails.io/docs/reference/project-config
|
||||
|
||||
## Live Development
|
||||
|
||||
To run in live development mode, run `wails dev` in the project directory. This will run a Vite development
|
||||
server that will provide very fast hot reload of your frontend changes. If you want to develop in a browser
|
||||
and have access to your Go methods, there is also a dev server that runs on http://localhost:34115. Connect
|
||||
to this in your browser, and you can call your Go code from devtools.
|
||||
|
||||
## Building
|
||||
|
||||
To build a redistributable, production mode package, use `wails build`.
|
||||
921
client/app.go
Normal file
921
client/app.go
Normal file
@@ -0,0 +1,921 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"client/internal/blocker"
|
||||
"client/internal/screen"
|
||||
"client/internal/winapi"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/wailsapp/wails/v2/pkg/runtime"
|
||||
)
|
||||
|
||||
var blockedReportLast sync.Map // processName -> time.Time
|
||||
var wifiReportLast sync.Map // ssidKey -> time.Time
|
||||
|
||||
var encryptionKey = []byte("simple-care-proctor-secretkey32!") // 32-byte key for AES-256
|
||||
|
||||
func encrypt(data []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := io.ReadFull(rand.Reader, nonce); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ciphertext := gcm.Seal(nonce, nonce, data, nil)
|
||||
return ciphertext, nil
|
||||
}
|
||||
|
||||
func decrypt(data []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(encryptionKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(data) < nonceSize {
|
||||
return nil, errors.New("ciphertext too short")
|
||||
}
|
||||
nonce, ciphertext := data[:nonceSize], data[nonceSize:]
|
||||
return gcm.Open(nil, nonce, ciphertext, nil)
|
||||
}
|
||||
|
||||
type StudentData struct {
|
||||
StudentID int64 `json:"studentId"`
|
||||
FullName string `json:"fullName"`
|
||||
Email string `json:"email"`
|
||||
Avatar string `json:"avatar"`
|
||||
Dob string `json:"dob"`
|
||||
StudentCode string `json:"studentCode"`
|
||||
Phone string `json:"phone"`
|
||||
SystemID int64 `json:"systemId"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
student *StudentData
|
||||
mu sync.Mutex
|
||||
sessionPath string
|
||||
statsPath string
|
||||
wsConn *websocket.Conn
|
||||
wsConnected bool
|
||||
wifiSSID string
|
||||
wifiBSSID string
|
||||
allowedApps string
|
||||
onlineSecs int
|
||||
offlineSecs int
|
||||
unsyncedOn int
|
||||
unsyncedOff int
|
||||
lastSyncTime time.Time
|
||||
isStreamingSc bool
|
||||
streamScStop chan struct{}
|
||||
statusMsg string
|
||||
expectingLogin bool
|
||||
backendOnline bool
|
||||
dashboard StudentDashboardSnapshot
|
||||
wifiEnforce bool
|
||||
acceptedWifi map[string]bool
|
||||
wifiRejected bool
|
||||
}
|
||||
|
||||
type LocalStats struct {
|
||||
OnlineSecs int `json:"onlineSecs"`
|
||||
OfflineSecs int `json:"offlineSecs"`
|
||||
UnsyncedOn int `json:"unsyncedOn"`
|
||||
UnsyncedOff int `json:"unsyncedOff"`
|
||||
}
|
||||
|
||||
type StudentShiftSnapshot struct {
|
||||
Period int `json:"period"`
|
||||
CourseName string `json:"courseName"`
|
||||
StartTime string `json:"startTime"`
|
||||
EndTime string `json:"endTime"`
|
||||
IsActiveNow bool `json:"isActiveNow"`
|
||||
OnlineSeconds int `json:"onlineSeconds"`
|
||||
OfflineSeconds int `json:"offlineSeconds"`
|
||||
AttendanceStatus int `json:"attendanceStatus"`
|
||||
AttendanceLabel string `json:"attendanceLabel"`
|
||||
}
|
||||
|
||||
type StudentDashboardSnapshot struct {
|
||||
SessionDate string `json:"sessionDate"`
|
||||
MonitorMode string `json:"monitorMode"`
|
||||
MonitorLabel string `json:"monitorLabel"`
|
||||
ClassRkID int64 `json:"classRkId"`
|
||||
ClassName string `json:"className"`
|
||||
ClassCode string `json:"classCode"`
|
||||
CurrentPeriod int `json:"currentPeriod"`
|
||||
CurrentCourseName string `json:"currentCourseName"`
|
||||
CurrentShiftStart string `json:"currentShiftStart"`
|
||||
CurrentShiftEnd string `json:"currentShiftEnd"`
|
||||
InScheduleNow bool `json:"inScheduleNow"`
|
||||
BlockerActive bool `json:"blockerActive"`
|
||||
Shifts []StudentShiftSnapshot `json:"shifts"`
|
||||
}
|
||||
|
||||
func NewApp() *App {
|
||||
configDir, err := os.UserConfigDir()
|
||||
if err != nil {
|
||||
// Fallback to executable folder if UserConfigDir is unavailable
|
||||
exePath, _ := os.Executable()
|
||||
configDir = filepath.Dir(exePath)
|
||||
}
|
||||
appDir := filepath.Join(configDir, "SimpleCare")
|
||||
_ = os.MkdirAll(appDir, 0755)
|
||||
|
||||
return &App{
|
||||
sessionPath: filepath.Join(appDir, "student_session.json"),
|
||||
statsPath: filepath.Join(appDir, "student_stats.json"),
|
||||
lastSyncTime: time.Now(),
|
||||
expectingLogin: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
a.loadSession()
|
||||
a.loadStats()
|
||||
|
||||
// Register blocker callbacks
|
||||
blocker.Instance.OnBlocked = func(procName string, title string) {
|
||||
go a.reportBlockedApp(procName, title)
|
||||
}
|
||||
blocker.Instance.OnKill = func(procName string, title string) {
|
||||
_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.WarningDialog,
|
||||
Title: "Ứng dụng bị đóng",
|
||||
Message: fmt.Sprintf("Hệ thống đã tự động đóng ứng dụng '%s' (%s) do không nằm trong danh sách các ứng dụng được phép sử dụng trong giờ học.", procName, title),
|
||||
})
|
||||
}
|
||||
|
||||
// Khởi chạy HTTP server nhận callback login thành công
|
||||
a.startLocalServer()
|
||||
|
||||
// Khởi chạy vòng lặp giám sát định kỳ (mỗi 10 giây)
|
||||
go a.monitorLoop()
|
||||
|
||||
// Khởi chạy vòng lặp đọc local storage của trang Rikkei Portal khi chưa đăng nhập
|
||||
go a.authStorageScanner()
|
||||
}
|
||||
|
||||
// startLocalServer khởi chạy server lắng nghe callback nhận thông tin sinh viên từ webview
|
||||
func (a *App) startLocalServer() {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("/login-success", func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, OPTIONS")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")
|
||||
if r.Method == "OPTIONS" {
|
||||
return
|
||||
}
|
||||
|
||||
data := r.URL.Query().Get("data")
|
||||
if data == "" {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
expecting := a.expectingLogin
|
||||
a.mu.Unlock()
|
||||
if !expecting {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ignored"))
|
||||
return
|
||||
}
|
||||
|
||||
var st StudentData
|
||||
if err := json.Unmarshal([]byte(data), &st); err == nil && st.StudentID > 0 {
|
||||
a.saveSession(&st)
|
||||
log.Printf("[LOCALSERVER] Student %s logged in successfully.", st.FullName)
|
||||
|
||||
a.mu.Lock()
|
||||
a.expectingLogin = false
|
||||
a.mu.Unlock()
|
||||
|
||||
blocker.Instance.Start()
|
||||
|
||||
// Kiểm tra điều kiện app được phép và giờ học trước khi tải giao diện
|
||||
go a.fetchAllowedApps(st.SystemID)
|
||||
go a.fetchStudentStatus(st.StudentID)
|
||||
go a.fetchWifiPolicy()
|
||||
|
||||
// Chuyển hướng WebView về trang dashboard của app bằng cách reload app assets
|
||||
runtime.WindowReloadApp(a.ctx)
|
||||
w.Write([]byte("ok"))
|
||||
} else {
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
}
|
||||
})
|
||||
|
||||
server := &http.Server{
|
||||
Addr: "127.0.0.1:34115",
|
||||
Handler: mux,
|
||||
}
|
||||
|
||||
go func() {
|
||||
_ = server.ListenAndServe()
|
||||
}()
|
||||
log.Println("[LOCALSERVER] Callback server started on http://127.0.0.1:34115")
|
||||
}
|
||||
|
||||
// loadSession nạp session từ file cục bộ
|
||||
func (a *App) loadSession() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(a.sessionPath)
|
||||
if err == nil {
|
||||
decrypted, errDec := decrypt(data)
|
||||
if errDec == nil {
|
||||
data = decrypted
|
||||
}
|
||||
var s StudentData
|
||||
if err := json.Unmarshal(data, &s); err == nil {
|
||||
a.student = &s
|
||||
log.Printf("[APP] Loaded local session: %s (%s)", s.FullName, s.StudentCode)
|
||||
blocker.Instance.Start()
|
||||
|
||||
go a.fetchAllowedApps(s.SystemID)
|
||||
go a.fetchStudentStatus(s.StudentID)
|
||||
go a.fetchWifiPolicy()
|
||||
go a.syncProfileToServer(&s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveSession lưu session ra file
|
||||
func (a *App) saveSession(s *StudentData) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
a.student = s
|
||||
data, _ := json.MarshalIndent(s, "", " ")
|
||||
encrypted, err := encrypt(data)
|
||||
if err == nil {
|
||||
_ = os.WriteFile(a.sessionPath, encrypted, 0644)
|
||||
} else {
|
||||
_ = os.WriteFile(a.sessionPath, data, 0644)
|
||||
}
|
||||
go a.syncProfileToServer(s)
|
||||
}
|
||||
|
||||
func (a *App) syncProfileToServer(s *StudentData) {
|
||||
if s == nil || s.Avatar == "" {
|
||||
return
|
||||
}
|
||||
payload := map[string]any{
|
||||
"studentRkId": s.StudentID,
|
||||
"classRkId": s.SystemID,
|
||||
"sessionDate": time.Now().Format("2006-01-02"),
|
||||
"avatar": s.Avatar,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Post("http://127.0.0.1:8080/api/student/sync-log", "application/json", bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
// loadStats nạp stats
|
||||
func (a *App) loadStats() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
data, err := os.ReadFile(a.statsPath)
|
||||
if err == nil {
|
||||
var ls LocalStats
|
||||
if err := json.Unmarshal(data, &ls); err == nil {
|
||||
a.onlineSecs = ls.OnlineSecs
|
||||
a.offlineSecs = ls.OfflineSecs
|
||||
a.unsyncedOn = ls.UnsyncedOn
|
||||
a.unsyncedOff = ls.UnsyncedOff
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// saveStats lưu stats ra file
|
||||
func (a *App) saveStats() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
ls := LocalStats{
|
||||
OnlineSecs: a.onlineSecs,
|
||||
OfflineSecs: a.offlineSecs,
|
||||
UnsyncedOn: a.unsyncedOn,
|
||||
UnsyncedOff: a.unsyncedOff,
|
||||
}
|
||||
data, _ := json.Marshal(ls)
|
||||
_ = os.WriteFile(a.statsPath, data, 0644)
|
||||
}
|
||||
|
||||
// CheckLoginStatus kiểm tra xem sinh viên đã đăng nhập chưa
|
||||
func (a *App) CheckLoginStatus() bool {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
return a.student != nil
|
||||
}
|
||||
|
||||
// GetStudentInfo trả về thông tin sinh viên
|
||||
func (a *App) GetStudentInfo() map[string]any {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.student == nil {
|
||||
return nil
|
||||
}
|
||||
return map[string]any{
|
||||
"studentId": a.student.StudentID,
|
||||
"fullName": a.student.FullName,
|
||||
"email": a.student.Email,
|
||||
"avatar": a.student.Avatar,
|
||||
"dob": a.student.Dob,
|
||||
"studentCode": a.student.StudentCode,
|
||||
"phone": a.student.Phone,
|
||||
"systemId": a.student.SystemID,
|
||||
}
|
||||
}
|
||||
|
||||
// NavigateToLogin chuyển WebView đến trang đăng nhập Rikkei Portal
|
||||
func (a *App) NavigateToLogin() {
|
||||
a.mu.Lock()
|
||||
a.expectingLogin = true
|
||||
a.mu.Unlock()
|
||||
runtime.WindowExecJS(a.ctx, "window.location.href = 'https://portal.rikkei.edu.vn/dangnhap'")
|
||||
}
|
||||
|
||||
// Logout đăng xuất sinh viên
|
||||
func (a *App) Logout() {
|
||||
a.mu.Lock()
|
||||
a.student = nil
|
||||
a.expectingLogin = false
|
||||
a.mu.Unlock()
|
||||
|
||||
_ = os.Remove(a.sessionPath)
|
||||
_ = os.Remove(a.statsPath)
|
||||
|
||||
a.mu.Lock()
|
||||
a.onlineSecs = 0
|
||||
a.offlineSecs = 0
|
||||
a.unsyncedOn = 0
|
||||
a.unsyncedOff = 0
|
||||
a.mu.Unlock()
|
||||
|
||||
blocker.Instance.Stop()
|
||||
a.disconnectWS()
|
||||
a.stopScreenshotStream()
|
||||
|
||||
runtime.WindowReloadApp(a.ctx)
|
||||
}
|
||||
|
||||
// GetStats trả về Wifi, thời gian online/offline và trạng thái giám sát
|
||||
func (a *App) GetStats() map[string]any {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
shifts := make([]StudentShiftSnapshot, len(a.dashboard.Shifts))
|
||||
copy(shifts, a.dashboard.Shifts)
|
||||
for i := range shifts {
|
||||
if shifts[i].IsActiveNow {
|
||||
shifts[i].OnlineSeconds += a.unsyncedOn
|
||||
shifts[i].OfflineSeconds += a.unsyncedOff
|
||||
}
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"wifiSSID": a.wifiSSID,
|
||||
"onlineSecs": a.onlineSecs,
|
||||
"offlineSecs": a.offlineSecs,
|
||||
"wsConnected": a.wsConnected,
|
||||
"serverReachable": a.backendOnline,
|
||||
"allowedApps": a.allowedApps,
|
||||
"statusMsg": a.statusMsg,
|
||||
"sessionDate": a.dashboard.SessionDate,
|
||||
"monitorMode": a.dashboard.MonitorMode,
|
||||
"monitorLabel": a.dashboard.MonitorLabel,
|
||||
"className": a.dashboard.ClassName,
|
||||
"classCode": a.dashboard.ClassCode,
|
||||
"currentPeriod": a.dashboard.CurrentPeriod,
|
||||
"currentCourseName": a.dashboard.CurrentCourseName,
|
||||
"currentShiftStart": a.dashboard.CurrentShiftStart,
|
||||
"currentShiftEnd": a.dashboard.CurrentShiftEnd,
|
||||
"inScheduleNow": a.dashboard.InScheduleNow,
|
||||
"blockerActive": a.dashboard.BlockerActive,
|
||||
"shifts": shifts,
|
||||
}
|
||||
}
|
||||
|
||||
// SendWebcamFrame truyền webcam frame từ JS frontend lên máy chủ qua WS
|
||||
func (a *App) SendWebcamFrame(frameBase64 string) {
|
||||
a.mu.Lock()
|
||||
conn := a.wsConn
|
||||
a.mu.Unlock()
|
||||
|
||||
if conn != nil {
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"event": "webcam_stream_frame",
|
||||
"data": map[string]any{
|
||||
"imageBuffer": frameBase64,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// authStorageScanner chỉ quét localStorage khi người dùng chủ động mở trang đăng nhập
|
||||
func (a *App) authStorageScanner() {
|
||||
ticker := time.NewTicker(1 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
|
||||
a.mu.Lock()
|
||||
expecting := a.expectingLogin
|
||||
loggedIn := a.student != nil
|
||||
a.mu.Unlock()
|
||||
|
||||
if loggedIn || !expecting {
|
||||
continue
|
||||
}
|
||||
|
||||
runtime.WindowExecJS(a.ctx, `
|
||||
try {
|
||||
const st = localStorage.getItem("student");
|
||||
if (st) {
|
||||
fetch("http://127.0.0.1:34115/login-success?data=" + encodeURIComponent(st));
|
||||
}
|
||||
} catch(e) {}
|
||||
`)
|
||||
}
|
||||
}
|
||||
|
||||
// monitorLoop định kỳ 10s: check wifi, ping backend, cập nhật online/offline seconds và đồng bộ lên server
|
||||
func (a *App) monitorLoop() {
|
||||
ticker := time.NewTicker(10 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
<-ticker.C
|
||||
|
||||
if !a.CheckLoginStatus() {
|
||||
continue
|
||||
}
|
||||
|
||||
wifi := winapi.GetWifiConnection()
|
||||
a.mu.Lock()
|
||||
a.wifiSSID = wifi.SSID
|
||||
a.wifiBSSID = wifi.BSSID
|
||||
a.mu.Unlock()
|
||||
|
||||
backendOnline := a.pingBackend()
|
||||
|
||||
if backendOnline && wifi.SSID != "" && wifi.BSSID != "" {
|
||||
go a.reportWifi(wifi.SSID, wifi.BSSID)
|
||||
a.fetchWifiPolicy()
|
||||
if !a.isWifiAllowed(wifi.SSID, wifi.BSSID) {
|
||||
a.rejectUnauthorizedWifi(wifi.SSID, wifi.BSSID)
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.backendOnline = backendOnline
|
||||
if backendOnline {
|
||||
a.onlineSecs += 10
|
||||
a.unsyncedOn += 10
|
||||
} else {
|
||||
a.offlineSecs += 10
|
||||
a.unsyncedOff += 10
|
||||
}
|
||||
a.mu.Unlock()
|
||||
|
||||
a.saveStats()
|
||||
|
||||
if backendOnline {
|
||||
a.connectWS()
|
||||
a.syncLogsToServer()
|
||||
} else {
|
||||
a.disconnectWS()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) pingBackend() bool {
|
||||
client := http.Client{Timeout: 3 * time.Second}
|
||||
resp, err := client.Get("http://127.0.0.1:8080/api/stats")
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
return resp.StatusCode == http.StatusOK
|
||||
}
|
||||
|
||||
func (a *App) syncLogsToServer() {
|
||||
a.mu.Lock()
|
||||
student := a.student
|
||||
unsyncedOn := a.unsyncedOn
|
||||
unsyncedOff := a.unsyncedOff
|
||||
ssid := a.wifiSSID
|
||||
a.mu.Unlock()
|
||||
|
||||
if student == nil {
|
||||
return
|
||||
}
|
||||
|
||||
go a.fetchStudentStatus(student.StudentID)
|
||||
go a.fetchAllowedApps(student.SystemID)
|
||||
|
||||
payload := map[string]any{
|
||||
"studentRkId": student.StudentID,
|
||||
"classRkId": student.SystemID,
|
||||
"sessionDate": time.Now().Format("2006-01-02"),
|
||||
"addOnlineSeconds": unsyncedOn,
|
||||
"addOfflineSeconds": unsyncedOff,
|
||||
"wifiSsid": ssid,
|
||||
"avatar": student.Avatar,
|
||||
}
|
||||
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
client := http.Client{Timeout: 5 * time.Second}
|
||||
resp, err := client.Post("http://127.0.0.1:8080/api/student/sync-log", "application/json", bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
a.mu.Lock()
|
||||
a.unsyncedOn -= unsyncedOn
|
||||
a.unsyncedOff -= unsyncedOff
|
||||
a.mu.Unlock()
|
||||
a.saveStats()
|
||||
log.Printf("[SYNC] Synced %ds online, %ds offline to server.", unsyncedOn, unsyncedOff)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) reportBlockedApp(procName string, title string) {
|
||||
a.mu.Lock()
|
||||
student := a.student
|
||||
a.mu.Unlock()
|
||||
if student == nil || student.StudentID <= 0 {
|
||||
return
|
||||
}
|
||||
|
||||
procKey := strings.ToLower(strings.TrimSpace(procName))
|
||||
if procKey == "" {
|
||||
return
|
||||
}
|
||||
if v, ok := blockedReportLast.Load(procKey); ok {
|
||||
if time.Since(v.(time.Time)) < 20*time.Second {
|
||||
return
|
||||
}
|
||||
}
|
||||
blockedReportLast.Store(procKey, time.Now())
|
||||
|
||||
payload := map[string]any{
|
||||
"studentRkId": student.StudentID,
|
||||
"processName": procName,
|
||||
"windowTitle": title,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
client := http.Client{Timeout: 4 * time.Second}
|
||||
resp, err := client.Post("http://127.0.0.1:8080/api/student/report-blocked-app", "application/json", bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
log.Printf("[CLIENT] report-blocked-app failed: %v", err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("[CLIENT] report-blocked-app status %d: %s", resp.StatusCode, string(body))
|
||||
return
|
||||
}
|
||||
log.Printf("[CLIENT] report-blocked-app ok: %s", procName)
|
||||
}
|
||||
|
||||
func (a *App) reportWifi(ssid, bssid string) {
|
||||
a.mu.Lock()
|
||||
student := a.student
|
||||
a.mu.Unlock()
|
||||
if student == nil || strings.TrimSpace(ssid) == "" || strings.TrimSpace(bssid) == "" {
|
||||
return
|
||||
}
|
||||
key := winapi.BSSIDKey(bssid)
|
||||
if len(key) != 12 {
|
||||
return
|
||||
}
|
||||
if v, ok := wifiReportLast.Load(key); ok {
|
||||
if time.Since(v.(time.Time)) < 30*time.Second {
|
||||
return
|
||||
}
|
||||
}
|
||||
wifiReportLast.Store(key, time.Now())
|
||||
|
||||
payload := map[string]any{
|
||||
"studentRkId": student.StudentID,
|
||||
"ssid": ssid,
|
||||
"bssid": bssid,
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
client := http.Client{Timeout: 4 * time.Second}
|
||||
resp, err := client.Post("http://127.0.0.1:8080/api/student/report-wifi", "application/json", bytes.NewBuffer(bodyBytes))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
func (a *App) fetchWifiPolicy() {
|
||||
client := http.Client{Timeout: 4 * time.Second}
|
||||
resp, err := client.Get("http://127.0.0.1:8080/api/student/wifi-policy")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
var res struct {
|
||||
Enforce bool `json:"enforce"`
|
||||
Accepted []struct {
|
||||
SSID string `json:"ssid"`
|
||||
BSSID string `json:"bssid"`
|
||||
} `json:"accepted"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return
|
||||
}
|
||||
allowed := make(map[string]bool, len(res.Accepted))
|
||||
for _, item := range res.Accepted {
|
||||
k := winapi.BSSIDKey(item.BSSID)
|
||||
if len(k) == 12 {
|
||||
allowed[k] = true
|
||||
}
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.wifiEnforce = res.Enforce
|
||||
a.acceptedWifi = allowed
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
func (a *App) isWifiAllowed(ssid, bssid string) bool {
|
||||
a.mu.Lock()
|
||||
enforce := a.wifiEnforce
|
||||
allowed := a.acceptedWifi
|
||||
a.mu.Unlock()
|
||||
|
||||
if !enforce || len(allowed) == 0 {
|
||||
return true
|
||||
}
|
||||
bKey := winapi.BSSIDKey(bssid)
|
||||
if bKey == "" || len(bKey) != 12 {
|
||||
return false
|
||||
}
|
||||
return allowed[bKey]
|
||||
}
|
||||
|
||||
func (a *App) rejectUnauthorizedWifi(ssid, bssid string) {
|
||||
a.mu.Lock()
|
||||
if a.wifiRejected {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
a.wifiRejected = true
|
||||
a.mu.Unlock()
|
||||
|
||||
blocker.Instance.Stop()
|
||||
a.disconnectWS()
|
||||
|
||||
_, _ = runtime.MessageDialog(a.ctx, runtime.MessageDialogOptions{
|
||||
Type: runtime.WarningDialog,
|
||||
Title: "WiFi không được phép",
|
||||
Message: fmt.Sprintf("Điểm phát WiFi \"%s\" (%s) không được phép.\n\nCó thể là mạng giả mạo (hotspot trùng tên). Hãy kết nối đúng WiFi của trường rồi mở lại ứng dụng.", ssid, bssid),
|
||||
})
|
||||
runtime.Quit(a.ctx)
|
||||
}
|
||||
|
||||
func (a *App) fetchAllowedApps(classId int64) {
|
||||
a.mu.Lock()
|
||||
studentID := int64(0)
|
||||
if a.student != nil {
|
||||
studentID = a.student.StudentID
|
||||
}
|
||||
a.mu.Unlock()
|
||||
|
||||
client := http.Client{Timeout: 4 * time.Second}
|
||||
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:8080/api/classes/%d/allowed-apps?studentId=%d", classId, studentID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
var res struct {
|
||||
Keywords string `json:"keywords"`
|
||||
Exit bool `json:"exit"`
|
||||
Reason string `json:"reason"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err == nil {
|
||||
if res.Exit {
|
||||
reason := res.Reason
|
||||
if reason == "" {
|
||||
reason = "Không đáp ứng điều kiện học tập (chưa cấu hình ứng dụng được phép hoặc ngoài giờ học)."
|
||||
}
|
||||
log.Printf("[CLIENT] Conditions not met: %s. Blocker suspended.", reason)
|
||||
|
||||
a.mu.Lock()
|
||||
a.allowedApps = ""
|
||||
a.statusMsg = reason
|
||||
a.mu.Unlock()
|
||||
|
||||
blocker.Instance.Stop()
|
||||
return
|
||||
}
|
||||
a.mu.Lock()
|
||||
a.allowedApps = res.Keywords
|
||||
if a.dashboard.MonitorLabel != "" {
|
||||
a.statusMsg = a.dashboard.MonitorLabel
|
||||
} else {
|
||||
a.statusMsg = "Đang trong giờ học"
|
||||
}
|
||||
a.mu.Unlock()
|
||||
|
||||
blocker.Instance.SetKeywords(res.Keywords)
|
||||
blocker.Instance.Start()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) fetchStudentStatus(studentID int64) {
|
||||
if studentID <= 0 {
|
||||
return
|
||||
}
|
||||
client := http.Client{Timeout: 4 * time.Second}
|
||||
resp, err := client.Get(fmt.Sprintf("http://127.0.0.1:8080/api/student/status?studentRkId=%d", studentID))
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return
|
||||
}
|
||||
|
||||
var res StudentDashboardSnapshot
|
||||
if err := json.NewDecoder(resp.Body).Decode(&res); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.dashboard = res
|
||||
if res.MonitorLabel != "" {
|
||||
a.statusMsg = res.MonitorLabel
|
||||
}
|
||||
if res.ClassName != "" && res.MonitorMode == "learning" {
|
||||
a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
|
||||
} else if res.ClassName != "" && res.MonitorMode == "outside_schedule" {
|
||||
a.statusMsg = fmt.Sprintf("%s · Ngoài giờ học", res.ClassName)
|
||||
} else if res.ClassName != "" && res.MonitorMode == "not_configured" {
|
||||
a.statusMsg = fmt.Sprintf("%s · %s", res.ClassName, res.MonitorLabel)
|
||||
}
|
||||
a.mu.Unlock()
|
||||
}
|
||||
|
||||
func (a *App) connectWS() {
|
||||
a.mu.Lock()
|
||||
if a.wsConnected || a.student == nil {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
student := a.student
|
||||
a.mu.Unlock()
|
||||
|
||||
wsUrl := fmt.Sprintf("ws://127.0.0.1:8080/ws?role=student&studentId=%d&classId=%d", student.StudentID, student.SystemID)
|
||||
dialer := websocket.Dialer{HandshakeTimeout: 4 * time.Second}
|
||||
conn, _, err := dialer.Dial(wsUrl, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.wsConn = conn
|
||||
a.wsConnected = true
|
||||
a.mu.Unlock()
|
||||
|
||||
log.Println("[WS] Connected to proctor websocket hub.")
|
||||
|
||||
go func() {
|
||||
defer a.disconnectWS()
|
||||
for {
|
||||
var msg struct {
|
||||
Event string `json:"event"`
|
||||
Data map[string]any `json:"data"`
|
||||
}
|
||||
err := conn.ReadJSON(&msg)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
|
||||
switch msg.Event {
|
||||
case "start_screenshot_stream":
|
||||
a.startScreenshotStream()
|
||||
case "stop_screenshot_stream":
|
||||
a.stopScreenshotStream()
|
||||
case "start_webcam_stream":
|
||||
runtime.EventsEmit(a.ctx, "start_webcam_stream")
|
||||
case "stop_webcam_stream":
|
||||
runtime.EventsEmit(a.ctx, "stop_webcam_stream")
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (a *App) disconnectWS() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if a.wsConn != nil {
|
||||
_ = a.wsConn.Close()
|
||||
a.wsConn = nil
|
||||
}
|
||||
a.wsConnected = false
|
||||
}
|
||||
|
||||
func (a *App) startScreenshotStream() {
|
||||
a.mu.Lock()
|
||||
if a.isStreamingSc {
|
||||
a.mu.Unlock()
|
||||
return
|
||||
}
|
||||
a.isStreamingSc = true
|
||||
a.streamScStop = make(chan struct{})
|
||||
a.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(250 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
frame, err := screen.CaptureScreen()
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
conn := a.wsConn
|
||||
a.mu.Unlock()
|
||||
|
||||
if conn != nil {
|
||||
_ = conn.WriteJSON(map[string]any{
|
||||
"event": "screenshot_stream_frame",
|
||||
"data": map[string]any{
|
||||
"imageBuffer": frame,
|
||||
},
|
||||
})
|
||||
}
|
||||
case <-a.streamScStop:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
log.Println("[WS] Screenshot screen-streaming started.")
|
||||
}
|
||||
|
||||
func (a *App) stopScreenshotStream() {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
if !a.isStreamingSc {
|
||||
return
|
||||
}
|
||||
a.isStreamingSc = false
|
||||
close(a.streamScStop)
|
||||
log.Println("[WS] Screenshot screen-streaming stopped.")
|
||||
}
|
||||
62
client/build.ps1
Normal file
62
client/build.ps1
Normal file
@@ -0,0 +1,62 @@
|
||||
# PowerShell script to build Wails client
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
Write-Host "=============================================" -ForegroundColor Cyan
|
||||
Write-Host " BUILDING WAILS CLIENT " -ForegroundColor Cyan
|
||||
Write-Host "=============================================" -ForegroundColor Cyan
|
||||
|
||||
# Check if wails is installed
|
||||
$wailsInstalled = $null -ne (Get-Command wails -ErrorAction SilentlyContinue)
|
||||
|
||||
if ($wailsInstalled) {
|
||||
Write-Host "[*] Found Wails CLI. Building via Wails..." -ForegroundColor Green
|
||||
try {
|
||||
wails build
|
||||
Write-Host "[+] Build completed successfully using Wails CLI!" -ForegroundColor Green
|
||||
exit 0
|
||||
} catch {
|
||||
Write-Host "[-] Wails build failed. Attempting manual fallback build..." -ForegroundColor Yellow
|
||||
}
|
||||
} else {
|
||||
Write-Host "[!] Wails CLI not found. Falling back to manual build..." -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Check for Go
|
||||
if ($null -eq (Get-Command go -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "[!] Go is not installed or not in PATH." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Check for npm
|
||||
if ($null -eq (Get-Command npm -ErrorAction SilentlyContinue)) {
|
||||
Write-Host "[!] Node.js/npm is not installed or not in PATH." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 1: Build Frontend
|
||||
Write-Host "[*] Building frontend assets..." -ForegroundColor Cyan
|
||||
Push-Location frontend
|
||||
|
||||
try {
|
||||
if (-not (Test-Path "node_modules")) {
|
||||
Write-Host "[*] node_modules not found. Running 'npm install'..." -ForegroundColor Cyan
|
||||
npm install
|
||||
}
|
||||
|
||||
Write-Host "[*] Running 'npm run build'..." -ForegroundColor Cyan
|
||||
npm run build
|
||||
} finally {
|
||||
Pop-Location
|
||||
}
|
||||
|
||||
# Step 2: Build Backend Go binary
|
||||
Write-Host "[*] Building Go application..." -ForegroundColor Cyan
|
||||
|
||||
try {
|
||||
# -H windowsgui: prevents console window from flashing on startup
|
||||
go build -ldflags="-s -w -H windowsgui" -o client.exe main.go app.go
|
||||
Write-Host "[+] Build completed successfully! Generated client.exe" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "[-] Go build failed." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
35
client/build/README.md
Normal file
35
client/build/README.md
Normal file
@@ -0,0 +1,35 @@
|
||||
# Build Directory
|
||||
|
||||
The build directory is used to house all the build files and assets for your application.
|
||||
|
||||
The structure is:
|
||||
|
||||
* bin - Output directory
|
||||
* darwin - macOS specific files
|
||||
* windows - Windows specific files
|
||||
|
||||
## Mac
|
||||
|
||||
The `darwin` directory holds files specific to Mac builds.
|
||||
These may be customised and used as part of the build. To return these files to the default state, simply delete them
|
||||
and
|
||||
build with `wails build`.
|
||||
|
||||
The directory contains the following files:
|
||||
|
||||
- `Info.plist` - the main plist file used for Mac builds. It is used when building using `wails build`.
|
||||
- `Info.dev.plist` - same as the main plist file but used when building using `wails dev`.
|
||||
|
||||
## Windows
|
||||
|
||||
The `windows` directory contains the manifest and rc files used when building with `wails build`.
|
||||
These may be customised for your application. To return these files to the default state, simply delete them and
|
||||
build with `wails build`.
|
||||
|
||||
- `icon.ico` - The icon used for the application. This is used when building using `wails build`. If you wish to
|
||||
use a different icon, simply replace this file with your own. If it is missing, a new `icon.ico` file
|
||||
will be created using the `appicon.png` file in the build directory.
|
||||
- `installer/*` - The files used to create the Windows installer. These are used when building using `wails build`.
|
||||
- `info.json` - Application details used for Windows builds. The data here will be used by the Windows installer,
|
||||
as well as the application itself (right click the exe -> properties -> details)
|
||||
- `wails.exe.manifest` - The main application manifest file.
|
||||
BIN
client/build/appicon.png
Normal file
BIN
client/build/appicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 130 KiB |
68
client/build/darwin/Info.dev.plist
Normal file
68
client/build/darwin/Info.dev.plist
Normal file
@@ -0,0 +1,68 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
<key>NSAppTransportSecurity</key>
|
||||
<dict>
|
||||
<key>NSAllowsLocalNetworking</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</dict>
|
||||
</plist>
|
||||
63
client/build/darwin/Info.plist
Normal file
63
client/build/darwin/Info.plist
Normal file
@@ -0,0 +1,63 @@
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>APPL</string>
|
||||
<key>CFBundleName</key>
|
||||
<string>{{.Info.ProductName}}</string>
|
||||
<key>CFBundleExecutable</key>
|
||||
<string>{{.OutputFilename}}</string>
|
||||
<key>CFBundleIdentifier</key>
|
||||
<string>com.wails.{{.Name}}</string>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleGetInfoString</key>
|
||||
<string>{{.Info.Comments}}</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
<string>{{.Info.ProductVersion}}</string>
|
||||
<key>CFBundleIconFile</key>
|
||||
<string>iconfile</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
<string>10.13.0</string>
|
||||
<key>NSHighResolutionCapable</key>
|
||||
<string>true</string>
|
||||
<key>NSHumanReadableCopyright</key>
|
||||
<string>{{.Info.Copyright}}</string>
|
||||
{{if .Info.FileAssociations}}
|
||||
<key>CFBundleDocumentTypes</key>
|
||||
<array>
|
||||
{{range .Info.FileAssociations}}
|
||||
<dict>
|
||||
<key>CFBundleTypeExtensions</key>
|
||||
<array>
|
||||
<string>{{.Ext}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeName</key>
|
||||
<string>{{.Name}}</string>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
<key>CFBundleTypeIconFile</key>
|
||||
<string>{{.IconName}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
{{if .Info.Protocols}}
|
||||
<key>CFBundleURLTypes</key>
|
||||
<array>
|
||||
{{range .Info.Protocols}}
|
||||
<dict>
|
||||
<key>CFBundleURLName</key>
|
||||
<string>com.wails.{{.Scheme}}</string>
|
||||
<key>CFBundleURLSchemes</key>
|
||||
<array>
|
||||
<string>{{.Scheme}}</string>
|
||||
</array>
|
||||
<key>CFBundleTypeRole</key>
|
||||
<string>{{.Role}}</string>
|
||||
</dict>
|
||||
{{end}}
|
||||
</array>
|
||||
{{end}}
|
||||
</dict>
|
||||
</plist>
|
||||
BIN
client/build/windows/icon.ico
Normal file
BIN
client/build/windows/icon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
15
client/build/windows/info.json
Normal file
15
client/build/windows/info.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"fixed": {
|
||||
"file_version": "{{.Info.ProductVersion}}"
|
||||
},
|
||||
"info": {
|
||||
"0000": {
|
||||
"ProductVersion": "{{.Info.ProductVersion}}",
|
||||
"CompanyName": "{{.Info.CompanyName}}",
|
||||
"FileDescription": "{{.Info.ProductName}}",
|
||||
"LegalCopyright": "{{.Info.Copyright}}",
|
||||
"ProductName": "{{.Info.ProductName}}",
|
||||
"Comments": "{{.Info.Comments}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
114
client/build/windows/installer/project.nsi
Normal file
114
client/build/windows/installer/project.nsi
Normal file
@@ -0,0 +1,114 @@
|
||||
Unicode true
|
||||
|
||||
####
|
||||
## Please note: Template replacements don't work in this file. They are provided with default defines like
|
||||
## mentioned underneath.
|
||||
## If the keyword is not defined, "wails_tools.nsh" will populate them with the values from ProjectInfo.
|
||||
## If they are defined here, "wails_tools.nsh" will not touch them. This allows to use this project.nsi manually
|
||||
## from outside of Wails for debugging and development of the installer.
|
||||
##
|
||||
## For development first make a wails nsis build to populate the "wails_tools.nsh":
|
||||
## > wails build --target windows/amd64 --nsis
|
||||
## Then you can call makensis on this file with specifying the path to your binary:
|
||||
## For a AMD64 only installer:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app.exe
|
||||
## For a ARM64 only installer:
|
||||
## > makensis -DARG_WAILS_ARM64_BINARY=..\..\bin\app.exe
|
||||
## For a installer with both architectures:
|
||||
## > makensis -DARG_WAILS_AMD64_BINARY=..\..\bin\app-amd64.exe -DARG_WAILS_ARM64_BINARY=..\..\bin\app-arm64.exe
|
||||
####
|
||||
## The following information is taken from the ProjectInfo file, but they can be overwritten here.
|
||||
####
|
||||
## !define INFO_PROJECTNAME "MyProject" # Default "{{.Name}}"
|
||||
## !define INFO_COMPANYNAME "MyCompany" # Default "{{.Info.CompanyName}}"
|
||||
## !define INFO_PRODUCTNAME "MyProduct" # Default "{{.Info.ProductName}}"
|
||||
## !define INFO_PRODUCTVERSION "1.0.0" # Default "{{.Info.ProductVersion}}"
|
||||
## !define INFO_COPYRIGHT "Copyright" # Default "{{.Info.Copyright}}"
|
||||
###
|
||||
## !define PRODUCT_EXECUTABLE "Application.exe" # Default "${INFO_PROJECTNAME}.exe"
|
||||
## !define UNINST_KEY_NAME "UninstKeyInRegistry" # Default "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
####
|
||||
## !define REQUEST_EXECUTION_LEVEL "admin" # Default "admin" see also https://nsis.sourceforge.io/Docs/Chapter4.html
|
||||
####
|
||||
## Include the wails tools
|
||||
####
|
||||
!include "wails_tools.nsh"
|
||||
|
||||
# The version information for this two must consist of 4 parts
|
||||
VIProductVersion "${INFO_PRODUCTVERSION}.0"
|
||||
VIFileVersion "${INFO_PRODUCTVERSION}.0"
|
||||
|
||||
VIAddVersionKey "CompanyName" "${INFO_COMPANYNAME}"
|
||||
VIAddVersionKey "FileDescription" "${INFO_PRODUCTNAME} Installer"
|
||||
VIAddVersionKey "ProductVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "FileVersion" "${INFO_PRODUCTVERSION}"
|
||||
VIAddVersionKey "LegalCopyright" "${INFO_COPYRIGHT}"
|
||||
VIAddVersionKey "ProductName" "${INFO_PRODUCTNAME}"
|
||||
|
||||
# Enable HiDPI support. https://nsis.sourceforge.io/Reference/ManifestDPIAware
|
||||
ManifestDPIAware true
|
||||
|
||||
!include "MUI.nsh"
|
||||
|
||||
!define MUI_ICON "..\icon.ico"
|
||||
!define MUI_UNICON "..\icon.ico"
|
||||
# !define MUI_WELCOMEFINISHPAGE_BITMAP "resources\leftimage.bmp" #Include this to add a bitmap on the left side of the Welcome Page. Must be a size of 164x314
|
||||
!define MUI_FINISHPAGE_NOAUTOCLOSE # Wait on the INSTFILES page so the user can take a look into the details of the installation steps
|
||||
!define MUI_ABORTWARNING # This will warn the user if they exit from the installer.
|
||||
|
||||
!insertmacro MUI_PAGE_WELCOME # Welcome to the installer page.
|
||||
# !insertmacro MUI_PAGE_LICENSE "resources\eula.txt" # Adds a EULA page to the installer
|
||||
!insertmacro MUI_PAGE_DIRECTORY # In which folder install page.
|
||||
!insertmacro MUI_PAGE_INSTFILES # Installing page.
|
||||
!insertmacro MUI_PAGE_FINISH # Finished installation page.
|
||||
|
||||
!insertmacro MUI_UNPAGE_INSTFILES # Uinstalling page
|
||||
|
||||
!insertmacro MUI_LANGUAGE "English" # Set the Language of the installer
|
||||
|
||||
## The following two statements can be used to sign the installer and the uninstaller. The path to the binaries are provided in %1
|
||||
#!uninstfinalize 'signtool --file "%1"'
|
||||
#!finalize 'signtool --file "%1"'
|
||||
|
||||
Name "${INFO_PRODUCTNAME}"
|
||||
OutFile "..\..\bin\${INFO_PROJECTNAME}-${ARCH}-installer.exe" # Name of the installer's file.
|
||||
InstallDir "$PROGRAMFILES64\${INFO_COMPANYNAME}\${INFO_PRODUCTNAME}" # Default installing folder ($PROGRAMFILES is Program Files folder).
|
||||
ShowInstDetails show # This will always show the installation details.
|
||||
|
||||
Function .onInit
|
||||
!insertmacro wails.checkArchitecture
|
||||
FunctionEnd
|
||||
|
||||
Section
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
!insertmacro wails.webview2runtime
|
||||
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!insertmacro wails.files
|
||||
|
||||
CreateShortcut "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
CreateShortCut "$DESKTOP\${INFO_PRODUCTNAME}.lnk" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
|
||||
!insertmacro wails.associateFiles
|
||||
!insertmacro wails.associateCustomProtocols
|
||||
|
||||
!insertmacro wails.writeUninstaller
|
||||
SectionEnd
|
||||
|
||||
Section "uninstall"
|
||||
!insertmacro wails.setShellContext
|
||||
|
||||
RMDir /r "$AppData\${PRODUCT_EXECUTABLE}" # Remove the WebView2 DataPath
|
||||
|
||||
RMDir /r $INSTDIR
|
||||
|
||||
Delete "$SMPROGRAMS\${INFO_PRODUCTNAME}.lnk"
|
||||
Delete "$DESKTOP\${INFO_PRODUCTNAME}.lnk"
|
||||
|
||||
!insertmacro wails.unassociateFiles
|
||||
!insertmacro wails.unassociateCustomProtocols
|
||||
|
||||
!insertmacro wails.deleteUninstaller
|
||||
SectionEnd
|
||||
249
client/build/windows/installer/wails_tools.nsh
Normal file
249
client/build/windows/installer/wails_tools.nsh
Normal file
@@ -0,0 +1,249 @@
|
||||
# DO NOT EDIT - Generated automatically by `wails build`
|
||||
|
||||
!include "x64.nsh"
|
||||
!include "WinVer.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
|
||||
!ifndef INFO_PROJECTNAME
|
||||
!define INFO_PROJECTNAME "{{.Name}}"
|
||||
!endif
|
||||
!ifndef INFO_COMPANYNAME
|
||||
!define INFO_COMPANYNAME "{{.Info.CompanyName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTNAME
|
||||
!define INFO_PRODUCTNAME "{{.Info.ProductName}}"
|
||||
!endif
|
||||
!ifndef INFO_PRODUCTVERSION
|
||||
!define INFO_PRODUCTVERSION "{{.Info.ProductVersion}}"
|
||||
!endif
|
||||
!ifndef INFO_COPYRIGHT
|
||||
!define INFO_COPYRIGHT "{{.Info.Copyright}}"
|
||||
!endif
|
||||
!ifndef PRODUCT_EXECUTABLE
|
||||
!define PRODUCT_EXECUTABLE "${INFO_PROJECTNAME}.exe"
|
||||
!endif
|
||||
!ifndef UNINST_KEY_NAME
|
||||
!define UNINST_KEY_NAME "${INFO_COMPANYNAME}${INFO_PRODUCTNAME}"
|
||||
!endif
|
||||
!define UNINST_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${UNINST_KEY_NAME}"
|
||||
|
||||
!ifndef REQUEST_EXECUTION_LEVEL
|
||||
!define REQUEST_EXECUTION_LEVEL "admin"
|
||||
!endif
|
||||
|
||||
RequestExecutionLevel "${REQUEST_EXECUTION_LEVEL}"
|
||||
|
||||
!ifdef ARG_WAILS_AMD64_BINARY
|
||||
!define SUPPORTS_AMD64
|
||||
!endif
|
||||
|
||||
!ifdef ARG_WAILS_ARM64_BINARY
|
||||
!define SUPPORTS_ARM64
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_AMD64
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "amd64_arm64"
|
||||
!else
|
||||
!define ARCH "amd64"
|
||||
!endif
|
||||
!else
|
||||
!ifdef SUPPORTS_ARM64
|
||||
!define ARCH "arm64"
|
||||
!else
|
||||
!error "Wails: Undefined ARCH, please provide at least one of ARG_WAILS_AMD64_BINARY or ARG_WAILS_ARM64_BINARY"
|
||||
!endif
|
||||
!endif
|
||||
|
||||
!macro wails.checkArchitecture
|
||||
!ifndef WAILS_WIN10_REQUIRED
|
||||
!define WAILS_WIN10_REQUIRED "This product is only supported on Windows 10 (Server 2016) and later."
|
||||
!endif
|
||||
|
||||
!ifndef WAILS_ARCHITECTURE_NOT_SUPPORTED
|
||||
!define WAILS_ARCHITECTURE_NOT_SUPPORTED "This product can't be installed on the current Windows architecture. Supports: ${ARCH}"
|
||||
!endif
|
||||
|
||||
${If} ${AtLeastWin10}
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
Goto ok
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
IfSilent silentArch notSilentArch
|
||||
silentArch:
|
||||
SetErrorLevel 65
|
||||
Abort
|
||||
notSilentArch:
|
||||
MessageBox MB_OK "${WAILS_ARCHITECTURE_NOT_SUPPORTED}"
|
||||
Quit
|
||||
${else}
|
||||
IfSilent silentWin notSilentWin
|
||||
silentWin:
|
||||
SetErrorLevel 64
|
||||
Abort
|
||||
notSilentWin:
|
||||
MessageBox MB_OK "${WAILS_WIN10_REQUIRED}"
|
||||
Quit
|
||||
${EndIf}
|
||||
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
!macro wails.files
|
||||
!ifdef SUPPORTS_AMD64
|
||||
${if} ${IsNativeAMD64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_AMD64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
|
||||
!ifdef SUPPORTS_ARM64
|
||||
${if} ${IsNativeARM64}
|
||||
File "/oname=${PRODUCT_EXECUTABLE}" "${ARG_WAILS_ARM64_BINARY}"
|
||||
${EndIf}
|
||||
!endif
|
||||
!macroend
|
||||
|
||||
!macro wails.writeUninstaller
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "Publisher" "${INFO_COMPANYNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayName" "${INFO_PRODUCTNAME}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayVersion" "${INFO_PRODUCTVERSION}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "DisplayIcon" "$INSTDIR\${PRODUCT_EXECUTABLE}"
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "UninstallString" "$\"$INSTDIR\uninstall.exe$\""
|
||||
WriteRegStr HKLM "${UNINST_KEY}" "QuietUninstallString" "$\"$INSTDIR\uninstall.exe$\" /S"
|
||||
|
||||
${GetSize} "$INSTDIR" "/S=0K" $0 $1 $2
|
||||
IntFmt $0 "0x%08X" $0
|
||||
WriteRegDWORD HKLM "${UNINST_KEY}" "EstimatedSize" "$0"
|
||||
!macroend
|
||||
|
||||
!macro wails.deleteUninstaller
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
|
||||
SetRegView 64
|
||||
DeleteRegKey HKLM "${UNINST_KEY}"
|
||||
!macroend
|
||||
|
||||
!macro wails.setShellContext
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "admin"
|
||||
SetShellVarContext all
|
||||
${else}
|
||||
SetShellVarContext current
|
||||
${EndIf}
|
||||
!macroend
|
||||
|
||||
# Install webview2 by launching the bootstrapper
|
||||
# See https://docs.microsoft.com/en-us/microsoft-edge/webview2/concepts/distribution#online-only-deployment
|
||||
!macro wails.webview2runtime
|
||||
!ifndef WAILS_INSTALL_WEBVIEW_DETAILPRINT
|
||||
!define WAILS_INSTALL_WEBVIEW_DETAILPRINT "Installing: WebView2 Runtime"
|
||||
!endif
|
||||
|
||||
SetRegView 64
|
||||
# If the admin key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKLM "SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
|
||||
${If} ${REQUEST_EXECUTION_LEVEL} == "user"
|
||||
# If the installer is run in user level, check the user specific key exists and is not empty then webview2 is already installed
|
||||
ReadRegStr $0 HKCU "Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" "pv"
|
||||
${If} $0 != ""
|
||||
Goto ok
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
|
||||
SetDetailsPrint both
|
||||
DetailPrint "${WAILS_INSTALL_WEBVIEW_DETAILPRINT}"
|
||||
SetDetailsPrint listonly
|
||||
|
||||
InitPluginsDir
|
||||
CreateDirectory "$pluginsdir\webview2bootstrapper"
|
||||
SetOutPath "$pluginsdir\webview2bootstrapper"
|
||||
File "tmp\MicrosoftEdgeWebview2Setup.exe"
|
||||
ExecWait '"$pluginsdir\webview2bootstrapper\MicrosoftEdgeWebview2Setup.exe" /silent /install'
|
||||
|
||||
SetDetailsPrint both
|
||||
ok:
|
||||
!macroend
|
||||
|
||||
# Copy of APP_ASSOCIATE and APP_UNASSOCIATE macros from here https://gist.github.com/nikku/281d0ef126dbc215dd58bfd5b3a5cd5b
|
||||
!macro APP_ASSOCIATE EXT FILECLASS DESCRIPTION ICON COMMANDTEXT COMMAND
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "${FILECLASS}_backup" "$R0"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "${FILECLASS}"
|
||||
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}" "" `${DESCRIPTION}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\DefaultIcon" "" `${ICON}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell" "" "open"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open" "" `${COMMANDTEXT}`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${FILECLASS}\shell\open\command" "" `${COMMAND}`
|
||||
!macroend
|
||||
|
||||
!macro APP_UNASSOCIATE EXT FILECLASS
|
||||
; Backup the previously associated file class
|
||||
ReadRegStr $R0 SHELL_CONTEXT "Software\Classes\.${EXT}" `${FILECLASS}_backup`
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\.${EXT}" "" "$R0"
|
||||
|
||||
DeleteRegKey SHELL_CONTEXT `Software\Classes\${FILECLASS}`
|
||||
!macroend
|
||||
|
||||
!macro wails.associateFiles
|
||||
; Create file associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_ASSOCIATE "{{.Ext}}" "{{.Name}}" "{{.Description}}" "$INSTDIR\{{.IconName}}.ico" "Open with ${INFO_PRODUCTNAME}" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
File "..\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateFiles
|
||||
; Delete app associations
|
||||
{{range .Info.FileAssociations}}
|
||||
!insertmacro APP_UNASSOCIATE "{{.Ext}}" "{{.Name}}"
|
||||
|
||||
Delete "$INSTDIR\{{.IconName}}.ico"
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_ASSOCIATE PROTOCOL DESCRIPTION ICON COMMAND
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "" "${DESCRIPTION}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}" "URL Protocol" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\DefaultIcon" "" "${ICON}"
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open" "" ""
|
||||
WriteRegStr SHELL_CONTEXT "Software\Classes\${PROTOCOL}\shell\open\command" "" "${COMMAND}"
|
||||
!macroend
|
||||
|
||||
!macro CUSTOM_PROTOCOL_UNASSOCIATE PROTOCOL
|
||||
DeleteRegKey SHELL_CONTEXT "Software\Classes\${PROTOCOL}"
|
||||
!macroend
|
||||
|
||||
!macro wails.associateCustomProtocols
|
||||
; Create custom protocols associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_ASSOCIATE "{{.Scheme}}" "{{.Description}}" "$INSTDIR\${PRODUCT_EXECUTABLE},0" "$INSTDIR\${PRODUCT_EXECUTABLE} $\"%1$\""
|
||||
|
||||
{{end}}
|
||||
!macroend
|
||||
|
||||
!macro wails.unassociateCustomProtocols
|
||||
; Delete app custom protocol associations
|
||||
{{range .Info.Protocols}}
|
||||
!insertmacro CUSTOM_PROTOCOL_UNASSOCIATE "{{.Scheme}}"
|
||||
{{end}}
|
||||
!macroend
|
||||
15
client/build/windows/wails.exe.manifest
Normal file
15
client/build/windows/wails.exe.manifest
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
|
||||
<assemblyIdentity type="win32" name="com.wails.{{.Name}}" version="{{.Info.ProductVersion}}.0" processorArchitecture="*"/>
|
||||
<dependency>
|
||||
<dependentAssembly>
|
||||
<assemblyIdentity type="win32" name="Microsoft.Windows.Common-Controls" version="6.0.0.0" processorArchitecture="*" publicKeyToken="6595b64144ccf1df" language="*"/>
|
||||
</dependentAssembly>
|
||||
</dependency>
|
||||
<asmv3:application>
|
||||
<asmv3:windowsSettings>
|
||||
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true/pm</dpiAware> <!-- fallback for Windows 7 and 8 -->
|
||||
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">permonitorv2,permonitor</dpiAwareness> <!-- falls back to per-monitor if per-monitor v2 is not supported -->
|
||||
</asmv3:windowsSettings>
|
||||
</asmv3:application>
|
||||
</assembly>
|
||||
BIN
client/client.exe
Normal file
BIN
client/client.exe
Normal file
Binary file not shown.
12
client/frontend/index.html
Normal file
12
client/frontend/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="vi">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||
<title>Simple Care — Rikkei Education</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js" type="module"></script>
|
||||
</body>
|
||||
</html>
|
||||
664
client/frontend/package-lock.json
generated
Normal file
664
client/frontend/package-lock.json
generated
Normal file
@@ -0,0 +1,664 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "frontend",
|
||||
"version": "0.0.0",
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.15.18.tgz",
|
||||
"integrity": "sha512-5GT+kcs2WVGjVs7+boataCkO5Fg0y4kCjzkB5bAip7H4jfnOS3dA6KPiww9W1OEKTKeAcUVhdZGvgI65OXmUnw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.15.18.tgz",
|
||||
"integrity": "sha512-L4jVKS82XVhw2nvzLg/19ClLWg0y27ulRwuP7lcyL6AbUWB5aPglXY3M21mauDQMDfRLs8cQmeT03r/+X3cZYQ==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/es-errors": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz",
|
||||
"integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.15.18.tgz",
|
||||
"integrity": "sha512-x/R72SmW3sSFRm5zrrIjAhCeQSAWoni3CmHEqfQrZIQTM3lVCdehdwuIqaOtfC2slvpdlLa62GYoN8SxT23m6Q==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/android-arm": "0.15.18",
|
||||
"@esbuild/linux-loong64": "0.15.18",
|
||||
"esbuild-android-64": "0.15.18",
|
||||
"esbuild-android-arm64": "0.15.18",
|
||||
"esbuild-darwin-64": "0.15.18",
|
||||
"esbuild-darwin-arm64": "0.15.18",
|
||||
"esbuild-freebsd-64": "0.15.18",
|
||||
"esbuild-freebsd-arm64": "0.15.18",
|
||||
"esbuild-linux-32": "0.15.18",
|
||||
"esbuild-linux-64": "0.15.18",
|
||||
"esbuild-linux-arm": "0.15.18",
|
||||
"esbuild-linux-arm64": "0.15.18",
|
||||
"esbuild-linux-mips64le": "0.15.18",
|
||||
"esbuild-linux-ppc64le": "0.15.18",
|
||||
"esbuild-linux-riscv64": "0.15.18",
|
||||
"esbuild-linux-s390x": "0.15.18",
|
||||
"esbuild-netbsd-64": "0.15.18",
|
||||
"esbuild-openbsd-64": "0.15.18",
|
||||
"esbuild-sunos-64": "0.15.18",
|
||||
"esbuild-windows-32": "0.15.18",
|
||||
"esbuild-windows-64": "0.15.18",
|
||||
"esbuild-windows-arm64": "0.15.18"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-64/-/esbuild-android-64-0.15.18.tgz",
|
||||
"integrity": "sha512-wnpt3OXRhcjfIDSZu9bnzT4/TNTDsOUvip0foZOUBG7QbSt//w3QV4FInVJxNhKc/ErhUxc5z4QjHtMi7/TbgA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-android-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-android-arm64/-/esbuild-android-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-G4xu89B8FCzav9XU8EjsXacCKSG2FT7wW9J6hOc18soEHJdtWu03L3TQDGf0geNxfLTtxENKBzMSq9LlbjS8OQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-64/-/esbuild-darwin-64-0.15.18.tgz",
|
||||
"integrity": "sha512-2WAvs95uPnVJPuYKP0Eqx+Dl/jaYseZEUUT1sjg97TJa4oBtbAKnPnl3b5M9l51/nbx7+QAEtuummJZW0sBEmg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-darwin-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-darwin-arm64/-/esbuild-darwin-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-tKPSxcTJ5OmNb1btVikATJ8NftlyNlc8BVNtyT/UAr62JFOhwHlnoPrhYWz09akBLHI9nElFVfWSTSRsrZiDUA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-64/-/esbuild-freebsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-TT3uBUxkteAjR1QbsmvSsjpKjOX6UkCstr8nMr+q7zi3NuZ1oIpa8U41Y8I8dJH2fJgdC3Dj3CXO5biLQpfdZA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-freebsd-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-freebsd-arm64/-/esbuild-freebsd-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-R/oVr+X3Tkh+S0+tL41wRMbdWtpWB8hEAMsOXDumSSa6qJR89U0S/PpLXrGF7Wk/JykfpWNokERUpCeHDl47wA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-32": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-32/-/esbuild-linux-32-0.15.18.tgz",
|
||||
"integrity": "sha512-lphF3HiCSYtaa9p1DtXndiQEeQDKPl9eN/XNoBf2amEghugNuqXNZA/ZovthNE2aa4EN43WroO0B85xVSjYkbg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-64/-/esbuild-linux-64-0.15.18.tgz",
|
||||
"integrity": "sha512-hNSeP97IviD7oxLKFuii5sDPJ+QHeiFTFLoLm7NZQligur8poNOWGIgpQ7Qf8Balb69hptMZzyOBIPtY09GZYw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm/-/esbuild-linux-arm-0.15.18.tgz",
|
||||
"integrity": "sha512-UH779gstRblS4aoS2qpMl3wjg7U0j+ygu3GjIeTonCcN79ZvpPee12Qun3vcdxX+37O5LFxz39XeW2I9bybMVA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-arm64/-/esbuild-linux-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-54qr8kg/6ilcxd+0V3h9rjT4qmjc0CccMVWrjOEM/pEcUzt8X62HfBSeZfT2ECpM7104mk4yfQXkosY8Quptug==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-mips64le": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-mips64le/-/esbuild-linux-mips64le-0.15.18.tgz",
|
||||
"integrity": "sha512-Mk6Ppwzzz3YbMl/ZZL2P0q1tnYqh/trYZ1VfNP47C31yT0K8t9s7Z077QrDA/guU60tGNp2GOwCQnp+DYv7bxQ==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-ppc64le": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-ppc64le/-/esbuild-linux-ppc64le-0.15.18.tgz",
|
||||
"integrity": "sha512-b0XkN4pL9WUulPTa/VKHx2wLCgvIAbgwABGnKMY19WhKZPT+8BxhZdqz6EgkqCLld7X5qiCY2F/bfpUUlnFZ9w==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-riscv64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-riscv64/-/esbuild-linux-riscv64-0.15.18.tgz",
|
||||
"integrity": "sha512-ba2COaoF5wL6VLZWn04k+ACZjZ6NYniMSQStodFKH/Pu6RxzQqzsmjR1t9QC89VYJxBeyVPTaHuBMCejl3O/xg==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-linux-s390x": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-linux-s390x/-/esbuild-linux-s390x-0.15.18.tgz",
|
||||
"integrity": "sha512-VbpGuXEl5FCs1wDVp93O8UIzl3ZrglgnSQ+Hu79g7hZu6te6/YHgVJxCM2SqfIila0J3k0csfnf8VD2W7u2kzQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-netbsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-netbsd-64/-/esbuild-netbsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-98ukeCdvdX7wr1vUYQzKo4kQ0N2p27H7I11maINv73fVEXt2kyh4K4m9f35U1K43Xc2QGXlzAw0K9yoU7JUjOg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-openbsd-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-openbsd-64/-/esbuild-openbsd-64-0.15.18.tgz",
|
||||
"integrity": "sha512-yK5NCcH31Uae076AyQAXeJzt/vxIo9+omZRKj1pauhk3ITuADzuOx5N2fdHrAKPxN+zH3w96uFKlY7yIn490xQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-sunos-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-sunos-64/-/esbuild-sunos-64-0.15.18.tgz",
|
||||
"integrity": "sha512-On22LLFlBeLNj/YF3FT+cXcyKPEI263nflYlAhz5crxtp3yRG1Ugfr7ITyxmCmjm4vbN/dGrb/B7w7U8yJR9yw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-32": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-32/-/esbuild-windows-32-0.15.18.tgz",
|
||||
"integrity": "sha512-o+eyLu2MjVny/nt+E0uPnBxYuJHBvho8vWsC2lV61A7wwTWC3jkN2w36jtA+yv1UgYkHRihPuQsL23hsCYGcOQ==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-64/-/esbuild-windows-64-0.15.18.tgz",
|
||||
"integrity": "sha512-qinug1iTTaIIrCorAUjR0fcBk24fjzEedFYhhispP8Oc7SFvs+XeW3YpAKiKp8dRpizl4YYAhxMjlftAMJiaUw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild-windows-arm64": {
|
||||
"version": "0.15.18",
|
||||
"resolved": "https://registry.npmjs.org/esbuild-windows-arm64/-/esbuild-windows-arm64-0.15.18.tgz",
|
||||
"integrity": "sha512-q9bsYzegpZcLziq0zgUi5KqGVtfhjxGbnksaBFYmWLxeV/S1fK4OLdq2DFYnXcLMjlZw2L0jLsk1eGoB522WXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
"integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"function-bind": "^1.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.16.2",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz",
|
||||
"integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"hasown": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.15",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz",
|
||||
"integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/path-parse": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz",
|
||||
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.16",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
||||
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.12",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve": {
|
||||
"version": "1.22.12",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||
"integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"es-errors": "^1.3.0",
|
||||
"is-core-module": "^2.16.1",
|
||||
"path-parse": "^1.0.7",
|
||||
"supports-preserve-symlinks-flag": "^1.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"resolve": "bin/resolve"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/rollup": {
|
||||
"version": "2.80.0",
|
||||
"resolved": "https://registry.npmjs.org/rollup/-/rollup-2.80.0.tgz",
|
||||
"integrity": "sha512-cIFJOD1DESzpjOBl763Kp1AH7UE/0fcdHe6rZXUdQ9c50uvgigvW97u3IcSeBwOkgqL/PXPBktBCh0KEu5L8XQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"rollup": "dist/bin/rollup"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-preserve-symlinks-flag": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz",
|
||||
"integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.4"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/vite": {
|
||||
"version": "3.2.11",
|
||||
"resolved": "https://registry.npmjs.org/vite/-/vite-3.2.11.tgz",
|
||||
"integrity": "sha512-K/jGKL/PgbIgKCiJo5QbASQhFiV02X9Jh+Qq0AKCRCRKZtOTVi4t6wh75FDpGf2N9rYOnzH87OEFQNaFy6pdxQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "^0.15.9",
|
||||
"postcss": "^8.4.18",
|
||||
"resolve": "^1.22.1",
|
||||
"rollup": "^2.79.1"
|
||||
},
|
||||
"bin": {
|
||||
"vite": "bin/vite.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.18.0 || >=16.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@types/node": ">= 14",
|
||||
"less": "*",
|
||||
"sass": "*",
|
||||
"stylus": "*",
|
||||
"sugarss": "*",
|
||||
"terser": "^5.4.0"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@types/node": {
|
||||
"optional": true
|
||||
},
|
||||
"less": {
|
||||
"optional": true
|
||||
},
|
||||
"sass": {
|
||||
"optional": true
|
||||
},
|
||||
"stylus": {
|
||||
"optional": true
|
||||
},
|
||||
"sugarss": {
|
||||
"optional": true
|
||||
},
|
||||
"terser": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
13
client/frontend/package.json
Normal file
13
client/frontend/package.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"name": "frontend",
|
||||
"private": true,
|
||||
"version": "0.0.0",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "^3.0.7"
|
||||
}
|
||||
}
|
||||
1
client/frontend/package.json.md5
Normal file
1
client/frontend/package.json.md5
Normal file
@@ -0,0 +1 @@
|
||||
5fbf12469d224a93954efecb5886e8a6
|
||||
691
client/frontend/src/app.css
Normal file
691
client/frontend/src/app.css
Normal file
@@ -0,0 +1,691 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:wght@400;500;600;700;800&family=Share+Tech+Mono&display=swap');
|
||||
|
||||
:root {
|
||||
--bg-main: #f5f7fa;
|
||||
--bg-card: #ffffff;
|
||||
--bg-subtle: #f0f3f7;
|
||||
--border-color: #e4e9f0;
|
||||
--accent: #bb2126;
|
||||
--accent-dark: #9e1c22;
|
||||
--accent-light: #fde8e9;
|
||||
--accent-glow: rgba(187, 33, 38, 0.12);
|
||||
--text-primary: #1a2332;
|
||||
--text-secondary: #5a6a7e;
|
||||
--text-muted: #8b99a8;
|
||||
--online-color: #0d9f6e;
|
||||
--online-light: #e6f7f1;
|
||||
--offline-color: #d63031;
|
||||
--offline-light: #fdeaea;
|
||||
--shadow-sm: 0 1px 3px rgba(26, 35, 50, 0.05);
|
||||
--shadow-md: 0 4px 16px rgba(26, 35, 50, 0.07);
|
||||
--radius-sm: 8px;
|
||||
--radius-md: 12px;
|
||||
--radius-lg: 16px;
|
||||
--transition: 0.2s ease;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
background-color: var(--bg-main);
|
||||
color: var(--text-primary);
|
||||
font-family: 'Be Vietnam Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Cards ── */
|
||||
.card {
|
||||
background: var(--bg-card);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
padding: 1.75rem;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
/* ── Login Screen ── */
|
||||
.login-prompt-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 1.5rem;
|
||||
background: var(--bg-main);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.login-prompt-container .card {
|
||||
max-width: 420px;
|
||||
width: 100%;
|
||||
max-height: calc(100vh - 3rem);
|
||||
overflow-y: auto;
|
||||
text-align: center;
|
||||
animation: floatIn 0.4s ease forwards;
|
||||
}
|
||||
|
||||
.login-brand-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.login-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
background: var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 0.9rem;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.login-brand-text {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.login-brand-name {
|
||||
font-size: 1.1rem;
|
||||
font-weight: 800;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.3px;
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.login-brand-sub {
|
||||
font-size: 0.72rem;
|
||||
font-weight: 500;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.login-title {
|
||||
font-size: 1.35rem;
|
||||
font-weight: 800;
|
||||
margin-bottom: 0.6rem;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.login-desc {
|
||||
color: var(--text-secondary);
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.65;
|
||||
margin-bottom: 1.75rem;
|
||||
}
|
||||
|
||||
/* ── Dashboard Layout ── */
|
||||
.dashboard-wrapper {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: 100vh;
|
||||
padding: 1.25rem 1.5rem;
|
||||
box-sizing: border-box;
|
||||
overflow: hidden;
|
||||
animation: fadeIn 0.35s ease-out;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 1rem;
|
||||
padding-bottom: 0.85rem;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
}
|
||||
|
||||
.brand-logo {
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
background: var(--accent);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-weight: 800;
|
||||
font-size: 0.85rem;
|
||||
color: white;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.brand-text {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.05rem;
|
||||
}
|
||||
|
||||
.brand-name {
|
||||
font-weight: 800;
|
||||
font-size: 1.05rem;
|
||||
letter-spacing: -0.3px;
|
||||
color: var(--text-primary);
|
||||
line-height: 1.2;
|
||||
}
|
||||
|
||||
.brand-sub {
|
||||
font-size: 0.7rem;
|
||||
font-weight: 500;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.main-grid {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(220px, 260px) 1fr;
|
||||
gap: 1rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* ── Profile Card ── */
|
||||
.profile-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
justify-content: flex-start;
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
margin-bottom: 1.25rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
width: 84px;
|
||||
height: 84px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
border: 3px solid var(--accent-light);
|
||||
padding: 3px;
|
||||
background: var(--bg-subtle);
|
||||
}
|
||||
|
||||
.status-indicator {
|
||||
position: absolute;
|
||||
bottom: 6px;
|
||||
right: 6px;
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
border-radius: 50%;
|
||||
border: 2.5px solid #fff;
|
||||
}
|
||||
|
||||
.status-indicator.online {
|
||||
background-color: var(--online-color);
|
||||
animation: pulseGreen 2s infinite;
|
||||
}
|
||||
|
||||
.status-indicator.offline {
|
||||
background-color: var(--offline-color);
|
||||
animation: pulseRed 2s infinite;
|
||||
}
|
||||
|
||||
.student-name {
|
||||
margin: 0;
|
||||
font-size: 1.2rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.3px;
|
||||
}
|
||||
|
||||
.student-code {
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
padding: 3px 10px;
|
||||
border-radius: 6px;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 700;
|
||||
margin-top: 0.4rem;
|
||||
margin-bottom: 0.5rem;
|
||||
border: 1px solid rgba(187, 33, 38, 0.15);
|
||||
letter-spacing: 0.3px;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.student-email {
|
||||
margin: 0;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.divider {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background: var(--border-color);
|
||||
margin: 1.25rem 0;
|
||||
}
|
||||
|
||||
.profile-info-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
font-size: 0.82rem;
|
||||
margin-bottom: 0.6rem;
|
||||
}
|
||||
|
||||
.profile-info-row span {
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.profile-info-row strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Right Column ── */
|
||||
.right-column {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.85rem;
|
||||
overflow: hidden;
|
||||
height: 100%;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.status-banner {
|
||||
padding: 0.75rem 1rem;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
gap: 0.75rem;
|
||||
border-left: 3px solid var(--accent);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-banner-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.status-banner-top {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.4rem 0.65rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.monitor-mode-tag {
|
||||
font-size: 0.68rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
padding: 0.15rem 0.45rem;
|
||||
border-radius: 4px;
|
||||
background: var(--accent-light);
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status-class-line {
|
||||
font-size: 0.78rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.status-banner-sub {
|
||||
font-size: 0.78rem;
|
||||
color: var(--text-muted);
|
||||
margin-top: 0.2rem;
|
||||
}
|
||||
|
||||
.status-banner.mode-learning { border-left-color: var(--online-color); }
|
||||
.status-banner.mode-learning .monitor-mode-tag { background: var(--online-light); color: var(--online-color); }
|
||||
.status-banner.mode-outside { border-left-color: #94a3b8; }
|
||||
.status-banner.mode-outside .monitor-mode-tag { background: #f1f5f9; color: #64748b; }
|
||||
.status-banner.mode-config { border-left-color: #f59e0b; }
|
||||
.status-banner.mode-config .monitor-mode-tag { background: #fffbeb; color: #b45309; }
|
||||
.status-banner.mode-exam { border-left-color: #7c3aed; }
|
||||
.status-banner.mode-exam .monitor-mode-tag { background: #f5f3ff; color: #6d28d9; }
|
||||
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 0.65rem;
|
||||
}
|
||||
|
||||
.stats-grid--two {
|
||||
grid-template-columns: 1fr 1fr;
|
||||
}
|
||||
|
||||
.connectivity-note {
|
||||
font-size: 0.75rem;
|
||||
line-height: 1.45;
|
||||
color: var(--text-muted);
|
||||
padding: 0.65rem 0.85rem;
|
||||
background: #f8fafc;
|
||||
border: 1px solid var(--border-color);
|
||||
}
|
||||
|
||||
.connectivity-note strong {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.conn-split {
|
||||
font-weight: 600;
|
||||
color: var(--online-color);
|
||||
}
|
||||
|
||||
.conn-split.offline-text {
|
||||
color: var(--offline-color);
|
||||
}
|
||||
|
||||
.card-title-row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
gap: 0.5rem;
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
|
||||
.card-title-sub {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
font-family: monospace;
|
||||
}
|
||||
|
||||
.clock-display-summary {
|
||||
margin-bottom: 0.75rem;
|
||||
padding-bottom: 0.75rem;
|
||||
border-bottom: 1px dashed var(--border-color);
|
||||
}
|
||||
|
||||
.shifts-wrap {
|
||||
overflow: auto;
|
||||
max-height: min(32vh, 220px);
|
||||
}
|
||||
|
||||
.shifts-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 0.76rem;
|
||||
}
|
||||
|
||||
.shifts-table th {
|
||||
text-align: left;
|
||||
padding: 0.4rem 0.35rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.shifts-table td {
|
||||
padding: 0.45rem 0.35rem;
|
||||
border-bottom: 1px solid #f1f5f9;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.shift-row-active {
|
||||
background: #f0fdf8;
|
||||
}
|
||||
|
||||
.shift-now {
|
||||
font-size: 0.65rem;
|
||||
color: var(--online-color);
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.shift-time {
|
||||
font-family: monospace;
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.time-online { color: var(--online-color); font-family: monospace; font-weight: 600; }
|
||||
.time-offline { color: var(--offline-color); font-family: monospace; }
|
||||
|
||||
.att-badge {
|
||||
display: inline-block;
|
||||
padding: 0.12rem 0.4rem;
|
||||
border-radius: 4px;
|
||||
font-size: 0.68rem;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.att-ok { background: var(--online-light); color: var(--online-color); }
|
||||
.att-late { background: #fffbeb; color: #b45309; }
|
||||
.att-leave { background: #eff6ff; color: #2563eb; }
|
||||
.att-absent { background: var(--offline-light); color: var(--offline-color); }
|
||||
.att-pending { background: #f1f5f9; color: #64748b; }
|
||||
|
||||
.shifts-empty {
|
||||
padding: 1rem;
|
||||
text-align: center;
|
||||
color: var(--text-muted);
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.status-banner-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: var(--accent-light);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.status-banner-label {
|
||||
font-size: 0.7rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.status-banner-value {
|
||||
font-size: 0.9rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
padding: 0.9rem 1rem;
|
||||
}
|
||||
|
||||
.stat-icon-wrap {
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
background: var(--bg-subtle);
|
||||
border-radius: var(--radius-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 1.2rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stat-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.15rem;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.stat-title {
|
||||
font-size: 0.72rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 0.95rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
/* ── Clocks ── */
|
||||
.clocks-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.65rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 0.85rem;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
letter-spacing: -0.2px;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.clock-display {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 0.85rem;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.clock-item {
|
||||
background: var(--bg-subtle);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 0.75rem 0.85rem;
|
||||
text-align: center;
|
||||
transition: var(--transition);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.clock-label {
|
||||
font-size: 0.68rem;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
margin-bottom: 0.4rem;
|
||||
letter-spacing: 0.04em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.clock-value {
|
||||
font-size: clamp(1.4rem, 3.5vw, 1.85rem);
|
||||
font-weight: 800;
|
||||
font-family: 'Share Tech Mono', monospace;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.clock-value.online {
|
||||
color: var(--online-color);
|
||||
}
|
||||
|
||||
.clock-value.offline {
|
||||
color: var(--offline-color);
|
||||
}
|
||||
|
||||
/* ── Buttons ── */
|
||||
.btn {
|
||||
padding: 0.65rem 1.15rem;
|
||||
border-radius: var(--radius-sm);
|
||||
border: none;
|
||||
font-weight: 600;
|
||||
font-size: 0.875rem;
|
||||
font-family: 'Be Vietnam Pro', sans-serif;
|
||||
cursor: pointer;
|
||||
transition: var(--transition);
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: var(--accent);
|
||||
color: white;
|
||||
box-shadow: 0 2px 8px var(--accent-glow);
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: var(--accent-dark);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 14px rgba(187, 33, 38, 0.25);
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: var(--bg-subtle);
|
||||
color: var(--text-secondary);
|
||||
border: 1px solid var(--border-color);
|
||||
padding: 0.5rem 0.9rem;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: var(--offline-light);
|
||||
color: var(--offline-color);
|
||||
border-color: rgba(214, 48, 49, 0.25);
|
||||
}
|
||||
|
||||
/* ── Animations ── */
|
||||
@keyframes floatIn {
|
||||
from { opacity: 0; transform: translateY(12px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
@keyframes pulseGreen {
|
||||
0% { box-shadow: 0 0 0 0 rgba(13, 159, 110, 0.35); }
|
||||
70% { box-shadow: 0 0 0 8px rgba(13, 159, 110, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(13, 159, 110, 0); }
|
||||
}
|
||||
|
||||
@keyframes pulseRed {
|
||||
0% { box-shadow: 0 0 0 0 rgba(214, 48, 49, 0.35); }
|
||||
70% { box-shadow: 0 0 0 8px rgba(214, 48, 49, 0); }
|
||||
100% { box-shadow: 0 0 0 0 rgba(214, 48, 49, 0); }
|
||||
}
|
||||
93
client/frontend/src/assets/fonts/OFL.txt
Normal file
93
client/frontend/src/assets/fonts/OFL.txt
Normal file
@@ -0,0 +1,93 @@
|
||||
Copyright 2016 The Nunito Project Authors (contact@sansoxygen.com),
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
http://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
BIN
client/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
BIN
client/frontend/src/assets/fonts/nunito-v16-latin-regular.woff2
Normal file
Binary file not shown.
BIN
client/frontend/src/assets/images/logo-universal.png
Normal file
BIN
client/frontend/src/assets/images/logo-universal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 136 KiB |
368
client/frontend/src/main.js
Normal file
368
client/frontend/src/main.js
Normal file
@@ -0,0 +1,368 @@
|
||||
import './style.css';
|
||||
import './app.css';
|
||||
|
||||
// Trạng thái cục bộ
|
||||
let loggedIn = false;
|
||||
let studentInfo = null;
|
||||
let stats = {
|
||||
wifiSSID: '',
|
||||
onlineSecs: 0,
|
||||
offlineSecs: 0,
|
||||
wsConnected: false,
|
||||
serverReachable: false,
|
||||
allowedApps: '',
|
||||
monitorMode: '',
|
||||
monitorLabel: '',
|
||||
className: '',
|
||||
classCode: '',
|
||||
currentPeriod: 0,
|
||||
currentCourseName: '',
|
||||
shifts: []
|
||||
};
|
||||
|
||||
// Quản lý webcam
|
||||
let webcamStream = null;
|
||||
let webcamInterval = null;
|
||||
const webcamVideo = document.createElement('video');
|
||||
webcamVideo.autoplay = true;
|
||||
webcamVideo.playsInline = true;
|
||||
webcamVideo.style.display = 'none';
|
||||
document.body.appendChild(webcamVideo);
|
||||
|
||||
const webcamCanvas = document.createElement('canvas');
|
||||
webcamCanvas.width = 320;
|
||||
webcamCanvas.height = 240;
|
||||
webcamCanvas.style.display = 'none';
|
||||
document.body.appendChild(webcamCanvas);
|
||||
|
||||
function init() {
|
||||
if (typeof window.go === 'undefined' || typeof window.go.main === 'undefined') {
|
||||
setTimeout(init, 200);
|
||||
return;
|
||||
}
|
||||
|
||||
window.runtime.EventsOn('start_webcam_stream', startWebcam);
|
||||
window.runtime.EventsOn('stop_webcam_stream', stopWebcam);
|
||||
checkLogin();
|
||||
}
|
||||
|
||||
async function checkLogin() {
|
||||
try {
|
||||
const isLogged = await window.go.main.App.CheckLoginStatus();
|
||||
if (isLogged) {
|
||||
loggedIn = true;
|
||||
studentInfo = await window.go.main.App.GetStudentInfo();
|
||||
renderDashboard();
|
||||
startStatsTicker();
|
||||
} else {
|
||||
loggedIn = false;
|
||||
renderLoginPrompt();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error checking login status:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function renderLoginPrompt() {
|
||||
document.querySelector('#app').innerHTML = `
|
||||
<div class="login-prompt-container">
|
||||
<div class="card">
|
||||
<div class="login-brand-row">
|
||||
<div class="login-logo">RE</div>
|
||||
<div class="login-brand-text">
|
||||
<div class="login-brand-name">Simple Care</div>
|
||||
<div class="login-brand-sub">Rikkei Education</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2 class="login-title">Đăng nhập sinh viên</h2>
|
||||
<p class="login-desc">
|
||||
Ứng dụng sẽ chuyển hướng bạn đến trang Rikkei Portal. Sau khi đăng nhập thành công, hệ thống sẽ tự động đồng bộ tài khoản học tập của bạn.
|
||||
</p>
|
||||
<button class="btn btn-primary" id="btn-goto-login" style="width: 100%; padding: 0.75rem;">
|
||||
Đi đến trang đăng nhập
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('btn-goto-login').addEventListener('click', () => {
|
||||
window.go.main.App.NavigateToLogin();
|
||||
});
|
||||
}
|
||||
|
||||
function monitorModeMeta(mode) {
|
||||
switch (mode) {
|
||||
case 'learning':
|
||||
return { icon: '🛡️', label: 'Đang giám sát', cls: 'mode-learning' };
|
||||
case 'exam':
|
||||
return { icon: '📝', label: 'Phòng thi', cls: 'mode-exam' };
|
||||
case 'outside_schedule':
|
||||
return { icon: '😴', label: 'Ngoài giờ học', cls: 'mode-outside' };
|
||||
case 'not_configured':
|
||||
return { icon: '⚙️', label: 'Chưa sẵn sàng', cls: 'mode-config' };
|
||||
default:
|
||||
return { icon: '⏳', label: 'Đang kết nối', cls: 'mode-pending' };
|
||||
}
|
||||
}
|
||||
|
||||
function attendanceClass(status) {
|
||||
if (status === 4) return 'att-ok';
|
||||
if (status === 3) return 'att-late';
|
||||
if (status === 1 || status === 2) return 'att-leave';
|
||||
if (status === 0) return 'att-absent';
|
||||
return 'att-pending';
|
||||
}
|
||||
|
||||
function renderShiftsTable(shifts) {
|
||||
if (!shifts || shifts.length === 0) {
|
||||
return '<div class="shifts-empty">Hôm nay không có ca học theo lịch lớp.</div>';
|
||||
}
|
||||
return `
|
||||
<table class="shifts-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Ca</th>
|
||||
<th>Môn học</th>
|
||||
<th>Giờ</th>
|
||||
<th>Trực tuyến</th>
|
||||
<th>Ngoại tuyến</th>
|
||||
<th>Điểm danh</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${shifts.map(s => `
|
||||
<tr class="${s.isActiveNow ? 'shift-row-active' : ''}">
|
||||
<td><strong>Ca ${s.period}</strong>${s.isActiveNow ? ' <span class="shift-now">đang học</span>' : ''}</td>
|
||||
<td>${s.courseName || '—'}</td>
|
||||
<td class="shift-time">${s.startTime}–${s.endTime}</td>
|
||||
<td class="time-online">${formatDuration(s.onlineSeconds || 0)}</td>
|
||||
<td class="time-offline">${formatDuration(s.offlineSeconds || 0)}</td>
|
||||
<td><span class="att-badge ${attendanceClass(s.attendanceStatus)}">${s.attendanceLabel || 'Chưa tính'}</span></td>
|
||||
</tr>
|
||||
`).join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
if (!studentInfo) return;
|
||||
|
||||
const mode = monitorModeMeta(stats.monitorMode);
|
||||
const classLine = stats.className
|
||||
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
|
||||
: 'Đang xác định lớp...';
|
||||
|
||||
document.querySelector('#app').innerHTML = `
|
||||
<div class="dashboard-wrapper">
|
||||
<header class="header">
|
||||
<div class="brand">
|
||||
<div class="brand-logo">RE</div>
|
||||
<div class="brand-text">
|
||||
<span class="brand-name">Simple Care</span>
|
||||
<span class="brand-sub">Rikkei Education</span>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-logout" id="btn-logout">Đăng xuất</button>
|
||||
</header>
|
||||
|
||||
<div class="main-grid">
|
||||
<div class="card profile-card">
|
||||
<div class="avatar-container">
|
||||
<img src="${studentInfo.avatar || 'https://via.placeholder.com/150'}" alt="Avatar" class="avatar" />
|
||||
<div class="status-indicator ${stats.serverReachable ? 'online' : 'offline'}"></div>
|
||||
</div>
|
||||
<h2 class="student-name">${studentInfo.fullName}</h2>
|
||||
<div class="student-code">${studentInfo.studentCode}</div>
|
||||
<p class="student-email">${studentInfo.email}</p>
|
||||
<div class="divider"></div>
|
||||
<div class="profile-info-row">
|
||||
<span>Lớp</span>
|
||||
<strong id="profile-class">${classLine}</strong>
|
||||
</div>
|
||||
<div class="profile-info-row">
|
||||
<span>Điện thoại</span>
|
||||
<strong>${studentInfo.phone || 'Chưa cung cấp'}</strong>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="right-column">
|
||||
<div class="card status-banner ${mode.cls}">
|
||||
<div class="status-banner-icon" id="status-icon">${mode.icon}</div>
|
||||
<div class="status-banner-main">
|
||||
<div class="status-banner-top">
|
||||
<span class="monitor-mode-tag" id="monitor-mode-tag">${mode.label}</span>
|
||||
<span class="status-class-line" id="status-class-line">${classLine}</span>
|
||||
</div>
|
||||
<div class="status-banner-value" id="status-message">${stats.monitorLabel || stats.statusMsg || 'Đang kết nối...'}</div>
|
||||
<div class="status-banner-sub" id="status-sub">
|
||||
${stats.currentPeriod > 0 && stats.currentCourseName
|
||||
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
|
||||
: 'Theo dõi theo lịch học của lớp'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-grid stats-grid--two">
|
||||
<div class="card stat-card">
|
||||
<div class="stat-icon-wrap">📡</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">WiFi</div>
|
||||
<div class="stat-value" id="wifi-ssid">${stats.wifiSSID || 'Đang quét...'}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card stat-card">
|
||||
<div class="stat-icon-wrap">🖥️</div>
|
||||
<div class="stat-info">
|
||||
<div class="stat-title">Máy chủ</div>
|
||||
<div class="stat-value" id="server-status" style="color: ${stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)'}">
|
||||
${stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clocks-card card">
|
||||
<div class="card-title-row">
|
||||
<div class="card-title">Hôm nay — theo ca</div>
|
||||
<div class="card-title-sub" id="session-date">${stats.sessionDate || ''}</div>
|
||||
</div>
|
||||
<div class="clock-display clock-display-summary">
|
||||
<div class="clock-item">
|
||||
<div class="clock-label">Tổng trực tuyến</div>
|
||||
<div class="clock-value online" id="clock-online">00:00:00</div>
|
||||
</div>
|
||||
<div class="clock-item">
|
||||
<div class="clock-label">Tổng ngoại tuyến</div>
|
||||
<div class="clock-value offline" id="clock-offline">00:00:00</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="shifts-wrap" id="shifts-wrap">
|
||||
${renderShiftsTable(stats.shifts)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
document.getElementById('btn-logout').addEventListener('click', async () => {
|
||||
if (confirm('Bạn có chắc chắn muốn đăng xuất khỏi ứng dụng giám sát?')) {
|
||||
loggedIn = false;
|
||||
studentInfo = null;
|
||||
stopWebcam();
|
||||
await window.go.main.App.Logout();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function formatDuration(totalSeconds) {
|
||||
const hrs = Math.floor(totalSeconds / 3600);
|
||||
const mins = Math.floor((totalSeconds % 3600) / 60);
|
||||
const secs = totalSeconds % 60;
|
||||
const pad = (num) => String(num).padStart(2, '0');
|
||||
return `${pad(hrs)}:${pad(mins)}:${pad(secs)}`;
|
||||
}
|
||||
|
||||
function startStatsTicker() {
|
||||
setInterval(async () => {
|
||||
if (!loggedIn) return;
|
||||
try {
|
||||
const freshStats = await window.go.main.App.GetStats();
|
||||
stats = { ...stats, ...freshStats };
|
||||
if (!Array.isArray(stats.shifts)) stats.shifts = [];
|
||||
|
||||
const wifiEl = document.getElementById('wifi-ssid');
|
||||
if (wifiEl) wifiEl.innerText = stats.wifiSSID || 'Không có WiFi';
|
||||
|
||||
const serverEl = document.getElementById('server-status');
|
||||
if (serverEl) {
|
||||
serverEl.innerText = stats.serverReachable ? 'Đã kết nối' : 'Mất kết nối';
|
||||
serverEl.style.color = stats.serverReachable ? 'var(--online-color)' : 'var(--offline-color)';
|
||||
}
|
||||
|
||||
const statusMsgEl = document.getElementById('status-message');
|
||||
if (statusMsgEl) {
|
||||
statusMsgEl.innerText = stats.monitorLabel || stats.statusMsg || 'Đang kết nối...';
|
||||
}
|
||||
|
||||
const mode = monitorModeMeta(stats.monitorMode);
|
||||
const statusIconEl = document.getElementById('status-icon');
|
||||
if (statusIconEl) statusIconEl.innerText = mode.icon;
|
||||
|
||||
const modeTagEl = document.getElementById('monitor-mode-tag');
|
||||
if (modeTagEl) modeTagEl.innerText = mode.label;
|
||||
|
||||
const banner = document.querySelector('.status-banner');
|
||||
if (banner) banner.className = `card status-banner ${mode.cls}`;
|
||||
|
||||
const classLine = stats.className
|
||||
? `${stats.className}${stats.classCode ? ` (${stats.classCode})` : ''}`
|
||||
: 'Đang xác định lớp...';
|
||||
const classLineEl = document.getElementById('status-class-line');
|
||||
if (classLineEl) classLineEl.innerText = classLine;
|
||||
const profileClassEl = document.getElementById('profile-class');
|
||||
if (profileClassEl) profileClassEl.innerText = classLine;
|
||||
|
||||
const statusSubEl = document.getElementById('status-sub');
|
||||
if (statusSubEl) {
|
||||
statusSubEl.innerText = stats.currentPeriod > 0 && stats.currentCourseName
|
||||
? `Ca ${stats.currentPeriod} · ${stats.currentCourseName}`
|
||||
: 'Theo dõi theo lịch học của lớp';
|
||||
}
|
||||
|
||||
const onlineEl = document.getElementById('clock-online');
|
||||
if (onlineEl) onlineEl.innerText = formatDuration(stats.onlineSecs || 0);
|
||||
|
||||
const offlineEl = document.getElementById('clock-offline');
|
||||
if (offlineEl) offlineEl.innerText = formatDuration(stats.offlineSecs || 0);
|
||||
|
||||
const shiftsWrap = document.getElementById('shifts-wrap');
|
||||
if (shiftsWrap) shiftsWrap.innerHTML = renderShiftsTable(stats.shifts);
|
||||
|
||||
const sessionDateEl = document.getElementById('session-date');
|
||||
if (sessionDateEl && stats.sessionDate) sessionDateEl.innerText = stats.sessionDate;
|
||||
|
||||
const indicator = document.querySelector('.status-indicator');
|
||||
if (indicator) {
|
||||
indicator.className = `status-indicator ${stats.serverReachable ? 'online' : 'offline'}`;
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error fetching stats:', err);
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
async function startWebcam() {
|
||||
if (webcamStream) return;
|
||||
try {
|
||||
webcamStream = await navigator.mediaDevices.getUserMedia({
|
||||
video: { width: 320, height: 240, frameRate: { max: 10 } }
|
||||
});
|
||||
webcamVideo.srcObject = webcamStream;
|
||||
webcamInterval = setInterval(() => {
|
||||
const ctx = webcamCanvas.getContext('2d');
|
||||
if (ctx) {
|
||||
ctx.drawImage(webcamVideo, 0, 0, webcamCanvas.width, webcamCanvas.height);
|
||||
const dataUrl = webcamCanvas.toDataURL('image/jpeg', 0.4);
|
||||
window.go.main.App.SendWebcamFrame(dataUrl);
|
||||
}
|
||||
}, 250);
|
||||
} catch (err) {
|
||||
console.error('Failed to open webcam:', err);
|
||||
}
|
||||
}
|
||||
|
||||
function stopWebcam() {
|
||||
if (webcamInterval) {
|
||||
clearInterval(webcamInterval);
|
||||
webcamInterval = null;
|
||||
}
|
||||
if (webcamStream) {
|
||||
webcamStream.getTracks().forEach(track => track.stop());
|
||||
webcamStream = null;
|
||||
}
|
||||
webcamVideo.srcObject = null;
|
||||
}
|
||||
|
||||
init();
|
||||
14
client/frontend/src/style.css
Normal file
14
client/frontend/src/style.css
Normal file
@@ -0,0 +1,14 @@
|
||||
html {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100%;
|
||||
}
|
||||
14
client/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
14
client/frontend/wailsjs/go/main/App.d.ts
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CheckLoginStatus():Promise<boolean>;
|
||||
|
||||
export function GetStats():Promise<Record<string, any>>;
|
||||
|
||||
export function GetStudentInfo():Promise<Record<string, any>>;
|
||||
|
||||
export function Logout():Promise<void>;
|
||||
|
||||
export function NavigateToLogin():Promise<void>;
|
||||
|
||||
export function SendWebcamFrame(arg1:string):Promise<void>;
|
||||
27
client/frontend/wailsjs/go/main/App.js
Normal file
27
client/frontend/wailsjs/go/main/App.js
Normal file
@@ -0,0 +1,27 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function CheckLoginStatus() {
|
||||
return window['go']['main']['App']['CheckLoginStatus']();
|
||||
}
|
||||
|
||||
export function GetStats() {
|
||||
return window['go']['main']['App']['GetStats']();
|
||||
}
|
||||
|
||||
export function GetStudentInfo() {
|
||||
return window['go']['main']['App']['GetStudentInfo']();
|
||||
}
|
||||
|
||||
export function Logout() {
|
||||
return window['go']['main']['App']['Logout']();
|
||||
}
|
||||
|
||||
export function NavigateToLogin() {
|
||||
return window['go']['main']['App']['NavigateToLogin']();
|
||||
}
|
||||
|
||||
export function SendWebcamFrame(arg1) {
|
||||
return window['go']['main']['App']['SendWebcamFrame'](arg1);
|
||||
}
|
||||
24
client/frontend/wailsjs/runtime/package.json
Normal file
24
client/frontend/wailsjs/runtime/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
330
client/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
330
client/frontend/wailsjs/runtime/runtime.d.ts
vendored
Normal file
@@ -0,0 +1,330 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
|
||||
// Notification types
|
||||
export interface NotificationOptions {
|
||||
id: string;
|
||||
title: string;
|
||||
subtitle?: string; // macOS and Linux only
|
||||
body?: string;
|
||||
categoryId?: string;
|
||||
data?: { [key: string]: any };
|
||||
}
|
||||
|
||||
export interface NotificationAction {
|
||||
id?: string;
|
||||
title?: string;
|
||||
destructive?: boolean; // macOS-specific
|
||||
}
|
||||
|
||||
export interface NotificationCategory {
|
||||
id?: string;
|
||||
actions?: NotificationAction[];
|
||||
hasReplyField?: boolean;
|
||||
replyPlaceholder?: string;
|
||||
replyButtonTitle?: string;
|
||||
}
|
||||
|
||||
// [InitializeNotifications](https://wails.io/docs/reference/runtime/notification#initializenotifications)
|
||||
// Initializes the notification service for the application.
|
||||
// This must be called before sending any notifications.
|
||||
export function InitializeNotifications(): Promise<void>;
|
||||
|
||||
// [CleanupNotifications](https://wails.io/docs/reference/runtime/notification#cleanupnotifications)
|
||||
// Cleans up notification resources and releases any held connections.
|
||||
export function CleanupNotifications(): Promise<void>;
|
||||
|
||||
// [IsNotificationAvailable](https://wails.io/docs/reference/runtime/notification#isnotificationavailable)
|
||||
// Checks if notifications are available on the current platform.
|
||||
export function IsNotificationAvailable(): Promise<boolean>;
|
||||
|
||||
// [RequestNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#requestnotificationauthorization)
|
||||
// Requests notification authorization from the user (macOS only).
|
||||
export function RequestNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [CheckNotificationAuthorization](https://wails.io/docs/reference/runtime/notification#checknotificationauthorization)
|
||||
// Checks the current notification authorization status (macOS only).
|
||||
export function CheckNotificationAuthorization(): Promise<boolean>;
|
||||
|
||||
// [SendNotification](https://wails.io/docs/reference/runtime/notification#sendnotification)
|
||||
// Sends a basic notification with the given options.
|
||||
export function SendNotification(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [SendNotificationWithActions](https://wails.io/docs/reference/runtime/notification#sendnotificationwithactions)
|
||||
// Sends a notification with action buttons. Requires a registered category.
|
||||
export function SendNotificationWithActions(options: NotificationOptions): Promise<void>;
|
||||
|
||||
// [RegisterNotificationCategory](https://wails.io/docs/reference/runtime/notification#registernotificationcategory)
|
||||
// Registers a notification category that can be used with SendNotificationWithActions.
|
||||
export function RegisterNotificationCategory(category: NotificationCategory): Promise<void>;
|
||||
|
||||
// [RemoveNotificationCategory](https://wails.io/docs/reference/runtime/notification#removenotificationcategory)
|
||||
// Removes a previously registered notification category.
|
||||
export function RemoveNotificationCategory(categoryId: string): Promise<void>;
|
||||
|
||||
// [RemoveAllPendingNotifications](https://wails.io/docs/reference/runtime/notification#removeallpendingnotifications)
|
||||
// Removes all pending notifications from the notification center.
|
||||
export function RemoveAllPendingNotifications(): Promise<void>;
|
||||
|
||||
// [RemovePendingNotification](https://wails.io/docs/reference/runtime/notification#removependingnotification)
|
||||
// Removes a specific pending notification by its identifier.
|
||||
export function RemovePendingNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveAllDeliveredNotifications](https://wails.io/docs/reference/runtime/notification#removealldeliverednotifications)
|
||||
// Removes all delivered notifications from the notification center.
|
||||
export function RemoveAllDeliveredNotifications(): Promise<void>;
|
||||
|
||||
// [RemoveDeliveredNotification](https://wails.io/docs/reference/runtime/notification#removedeliverednotification)
|
||||
// Removes a specific delivered notification by its identifier.
|
||||
export function RemoveDeliveredNotification(identifier: string): Promise<void>;
|
||||
|
||||
// [RemoveNotification](https://wails.io/docs/reference/runtime/notification#removenotification)
|
||||
// Removes a notification by its identifier (cross-platform convenience function).
|
||||
export function RemoveNotification(identifier: string): Promise<void>;
|
||||
298
client/frontend/wailsjs/runtime/runtime.js
Normal file
298
client/frontend/wailsjs/runtime/runtime.js
Normal file
@@ -0,0 +1,298 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
|
||||
export function InitializeNotifications() {
|
||||
return window.runtime.InitializeNotifications();
|
||||
}
|
||||
|
||||
export function CleanupNotifications() {
|
||||
return window.runtime.CleanupNotifications();
|
||||
}
|
||||
|
||||
export function IsNotificationAvailable() {
|
||||
return window.runtime.IsNotificationAvailable();
|
||||
}
|
||||
|
||||
export function RequestNotificationAuthorization() {
|
||||
return window.runtime.RequestNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function CheckNotificationAuthorization() {
|
||||
return window.runtime.CheckNotificationAuthorization();
|
||||
}
|
||||
|
||||
export function SendNotification(options) {
|
||||
return window.runtime.SendNotification(options);
|
||||
}
|
||||
|
||||
export function SendNotificationWithActions(options) {
|
||||
return window.runtime.SendNotificationWithActions(options);
|
||||
}
|
||||
|
||||
export function RegisterNotificationCategory(category) {
|
||||
return window.runtime.RegisterNotificationCategory(category);
|
||||
}
|
||||
|
||||
export function RemoveNotificationCategory(categoryId) {
|
||||
return window.runtime.RemoveNotificationCategory(categoryId);
|
||||
}
|
||||
|
||||
export function RemoveAllPendingNotifications() {
|
||||
return window.runtime.RemoveAllPendingNotifications();
|
||||
}
|
||||
|
||||
export function RemovePendingNotification(identifier) {
|
||||
return window.runtime.RemovePendingNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveAllDeliveredNotifications() {
|
||||
return window.runtime.RemoveAllDeliveredNotifications();
|
||||
}
|
||||
|
||||
export function RemoveDeliveredNotification(identifier) {
|
||||
return window.runtime.RemoveDeliveredNotification(identifier);
|
||||
}
|
||||
|
||||
export function RemoveNotification(identifier) {
|
||||
return window.runtime.RemoveNotification(identifier);
|
||||
}
|
||||
44
client/go.mod
Normal file
44
client/go.mod
Normal file
@@ -0,0 +1,44 @@
|
||||
module client
|
||||
|
||||
go 1.23.0
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018
|
||||
github.com/wailsapp/wails/v2 v2.12.0
|
||||
)
|
||||
|
||||
require (
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 // indirect
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/gen2brain/shm v0.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/jezek/xgb v1.1.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.33.0 // indirect
|
||||
golang.org/x/net v0.35.0 // indirect
|
||||
golang.org/x/sys v0.30.0 // indirect
|
||||
golang.org/x/text v0.22.0 // indirect
|
||||
)
|
||||
|
||||
// replace github.com/wailsapp/wails/v2 v2.12.0 => C:\Users\PhuocNTB\go\pkg\mod
|
||||
92
client/go.sum
Normal file
92
client/go.sum
Normal file
@@ -0,0 +1,92 @@
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3 h1:N3IGoHHp9pb6mj1cbXbuaSXV/UMKwmbKLf53nQmtqMA=
|
||||
git.sr.ht/~jackmordaunt/go-toast/v2 v2.0.3/go.mod h1:QtOLZGz8olr4qH2vWK0QH0w0O4T9fEIjMuWpKUsH7nc=
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/gen2brain/shm v0.1.0 h1:MwPeg+zJQXN0RM9o+HqaSFypNoNEcNpeoGp0BTSx2YY=
|
||||
github.com/gen2brain/shm v0.1.0/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018 h1:NQYgMY188uWrS+E/7xMVpydsI48PMHcc7SfR4OxkDF4=
|
||||
github.com/kbinani/screenshot v0.0.0-20250624051815-089614a94018/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.12.0 h1:BHO/kLNWFHYjCzucxbzAYZWUjub1Tvb4cSguQozHn5c=
|
||||
github.com/wailsapp/wails/v2 v2.12.0/go.mod h1:mo1bzK1DEJrobt7YrBjgxvb5Sihb1mhAY09hppbibQg=
|
||||
golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus=
|
||||
golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8=
|
||||
golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc=
|
||||
golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
|
||||
golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
317
client/internal/blocker/blocker.go
Normal file
317
client/internal/blocker/blocker.go
Normal file
@@ -0,0 +1,317 @@
|
||||
package blocker
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var (
|
||||
user32 = syscall.NewLazyDLL("user32.dll")
|
||||
procEnumWindows = user32.NewProc("EnumWindows")
|
||||
procIsWindowVisible = user32.NewProc("IsWindowVisible")
|
||||
procGetWindowTextW = user32.NewProc("GetWindowTextW")
|
||||
procGetWindowTextLengthW = user32.NewProc("GetWindowTextLengthW")
|
||||
procGetWindowThreadProcessId = user32.NewProc("GetWindowThreadProcessId")
|
||||
procGetWindow = user32.NewProc("GetWindow")
|
||||
procGetWindowLongW = user32.NewProc("GetWindowLongW")
|
||||
|
||||
dwmapi = syscall.NewLazyDLL("dwmapi.dll")
|
||||
procDwmGetWindowAttribute = dwmapi.NewProc("DwmGetWindowAttribute")
|
||||
|
||||
kernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
)
|
||||
|
||||
const (
|
||||
GW_OWNER = 4
|
||||
WS_EX_TOOLWINDOW = 0x00000080
|
||||
DWMWA_CLOAKED = 14
|
||||
)
|
||||
|
||||
func isRealGUIWindow(hwnd uintptr) bool {
|
||||
// 1. Phải đang hiển thị
|
||||
ret, _, _ := procIsWindowVisible.Call(hwnd)
|
||||
if ret == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 2. Không được có chủ sở hữu (phải là cửa sổ chính - top-level)
|
||||
owner, _, _ := procGetWindow.Call(hwnd, GW_OWNER)
|
||||
if owner != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 3. Không phải là tool window (WS_EX_TOOLWINDOW)
|
||||
gwlExStyle := int32(-20) // GWL_EXSTYLE = -20
|
||||
style, _, _ := procGetWindowLongW.Call(hwnd, uintptr(gwlExStyle))
|
||||
if (style & WS_EX_TOOLWINDOW) != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 4. Không bị cloaked bởi DWM (ví dụ: app UWP bị treo/chạy ngầm, màn hình ảo)
|
||||
var cloaked uint32
|
||||
hr, _, _ := procDwmGetWindowAttribute.Call(hwnd, DWMWA_CLOAKED, uintptr(unsafe.Pointer(&cloaked)), 4)
|
||||
if hr == 0 && cloaked != 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
type WindowInfo struct {
|
||||
PID uint32
|
||||
Title string
|
||||
ProcessName string
|
||||
}
|
||||
|
||||
type Blocker struct {
|
||||
mu sync.Mutex
|
||||
allowedKeywords []string
|
||||
running bool
|
||||
stopChan chan struct{}
|
||||
OnBlocked func(procName string, title string)
|
||||
OnKill func(procName string, title string)
|
||||
}
|
||||
|
||||
var Instance = &Blocker{
|
||||
allowedKeywords: []string{"chrome", "idea64", "vscode", "wails", "simple_care", "client"},
|
||||
stopChan: make(chan struct{}),
|
||||
}
|
||||
|
||||
func getProcessMap() (map[uint32]string, error) {
|
||||
snapshot, err := syscall.CreateToolhelp32Snapshot(syscall.TH32CS_SNAPPROCESS, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer syscall.CloseHandle(snapshot)
|
||||
|
||||
var pe syscall.ProcessEntry32
|
||||
pe.Size = uint32(unsafe.Sizeof(pe))
|
||||
|
||||
err = syscall.Process32First(snapshot, &pe)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
pm := make(map[uint32]string)
|
||||
for {
|
||||
name := syscall.UTF16ToString(pe.ExeFile[:])
|
||||
pm[pe.ProcessID] = name
|
||||
|
||||
err = syscall.Process32Next(snapshot, &pe)
|
||||
if err != nil {
|
||||
break
|
||||
}
|
||||
}
|
||||
return pm, nil
|
||||
}
|
||||
|
||||
func getWindowText(hwnd uintptr) string {
|
||||
length, _, _ := procGetWindowTextLengthW.Call(hwnd)
|
||||
if length == 0 {
|
||||
return ""
|
||||
}
|
||||
buf := make([]uint16, length+1)
|
||||
procGetWindowTextW.Call(hwnd, uintptr(unsafe.Pointer(&buf[0])), length+1)
|
||||
return syscall.UTF16ToString(buf)
|
||||
}
|
||||
|
||||
var (
|
||||
enumWindowsMutex sync.Mutex
|
||||
enumWindowsList []WindowInfo
|
||||
enumProcessMap map[uint32]string
|
||||
)
|
||||
|
||||
var enumWindowsCallback = syscall.NewCallback(func(hwnd uintptr, lParam uintptr) uintptr {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
log.Printf("[BLOCKER] Callback panic recovered: %v", r)
|
||||
}
|
||||
}()
|
||||
|
||||
if !isRealGUIWindow(hwnd) {
|
||||
return 1
|
||||
}
|
||||
|
||||
title := getWindowText(hwnd)
|
||||
if title == "" {
|
||||
return 1
|
||||
}
|
||||
|
||||
var pid uint32
|
||||
procGetWindowThreadProcessId.Call(hwnd, uintptr(unsafe.Pointer(&pid)))
|
||||
|
||||
procName := enumProcessMap[pid]
|
||||
if procName == "" {
|
||||
procName = "Unknown"
|
||||
}
|
||||
|
||||
enumWindowsList = append(enumWindowsList, WindowInfo{
|
||||
PID: pid,
|
||||
Title: title,
|
||||
ProcessName: procName,
|
||||
})
|
||||
|
||||
return 1
|
||||
})
|
||||
|
||||
func EnumerateGUIWindows() ([]WindowInfo, error) {
|
||||
pMap, err := getProcessMap()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
enumWindowsMutex.Lock()
|
||||
defer enumWindowsMutex.Unlock()
|
||||
|
||||
enumWindowsList = make([]WindowInfo, 0, 100)
|
||||
enumProcessMap = pMap
|
||||
|
||||
procEnumWindows.Call(enumWindowsCallback, 0)
|
||||
|
||||
// Clean up map reference so GC can reclaim it
|
||||
enumProcessMap = nil
|
||||
|
||||
// Copy to a new slice to return safely
|
||||
res := make([]WindowInfo, len(enumWindowsList))
|
||||
copy(res, enumWindowsList)
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (b *Blocker) SetKeywords(keywords string) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
parts := strings.Split(keywords, ",")
|
||||
var list []string
|
||||
for _, p := range parts {
|
||||
trimmed := strings.TrimSpace(strings.ToLower(p))
|
||||
if trimmed != "" {
|
||||
list = append(list, trimmed)
|
||||
}
|
||||
}
|
||||
b.allowedKeywords = list
|
||||
log.Printf("[BLOCKER] Keywords updated: %v", b.allowedKeywords)
|
||||
}
|
||||
|
||||
var systemAllowed = map[string]bool{
|
||||
"explorer.exe": true,
|
||||
"taskmgr.exe": true,
|
||||
"cmd.exe": true,
|
||||
"powershell.exe": true,
|
||||
"conhost.exe": true,
|
||||
"client.exe": true, // App Wails của ta
|
||||
"wails.exe": true,
|
||||
"msedgewebview2.exe": true, // WebView2 runtime của Wails
|
||||
"code.exe": true, // VSCode
|
||||
"cursor.exe": true, // Cursor
|
||||
"windsurf.exe": true, // Windsurf
|
||||
"goland.exe": true, // GoLand
|
||||
"goland64.exe": true, // GoLand
|
||||
"idea64.exe": true, // IntelliJ IDEA
|
||||
"clion64.exe": true, // CLion
|
||||
"webstorm64.exe": true, // WebStorm
|
||||
"pycharm64.exe": true, // PyCharm
|
||||
"rider64.exe": true, // Rider
|
||||
"studio64.exe": true, // Android Studio
|
||||
"eclipse.exe": true, // Eclipse
|
||||
"sublime_text.exe": true, // Sublime Text
|
||||
"notepad++.exe": true, // Notepad++
|
||||
"devenv.exe": true, // Visual Studio
|
||||
"git-bash.exe": true, // Git Bash
|
||||
"bash.exe": true, // Bash
|
||||
}
|
||||
|
||||
func (b *Blocker) checkAndKill() {
|
||||
b.mu.Lock()
|
||||
keywords := make([]string, len(b.allowedKeywords))
|
||||
copy(keywords, b.allowedKeywords)
|
||||
b.mu.Unlock()
|
||||
|
||||
// Nếu không cấu hình keyword thì không chặn gì cả
|
||||
if len(keywords) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
windows, err := EnumerateGUIWindows()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
for _, w := range windows {
|
||||
pNameLower := strings.ToLower(w.ProcessName)
|
||||
wTitleLower := strings.ToLower(w.Title)
|
||||
|
||||
// 1. Luôn cho phép hệ thống/app cốt lõi hoặc chính tiến trình này
|
||||
if w.PID == uint32(os.Getpid()) || systemAllowed[pNameLower] || strings.Contains(pNameLower, "antigravity") || strings.Contains(wTitleLower, "antigravity") {
|
||||
continue
|
||||
}
|
||||
|
||||
// 2. Kiểm tra xem có chứa bất kỳ từ khóa nào được cho phép không
|
||||
allowed := false
|
||||
for _, kw := range keywords {
|
||||
if strings.Contains(pNameLower, kw) || strings.Contains(wTitleLower, kw) {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Nếu không nằm trong whitelist, tắt ứng dụng
|
||||
if !allowed {
|
||||
if b.OnBlocked != nil {
|
||||
b.OnBlocked(w.ProcessName, w.Title)
|
||||
}
|
||||
log.Printf("[BLOCKER] KILLED unauthorized application: %s (PID: %d, Title: %s)", w.ProcessName, w.PID, w.Title)
|
||||
h, err := syscall.OpenProcess(syscall.PROCESS_TERMINATE, false, w.PID)
|
||||
if err == nil {
|
||||
errTerm := syscall.TerminateProcess(h, 0)
|
||||
_ = syscall.CloseHandle(h)
|
||||
if errTerm == nil && b.OnKill != nil {
|
||||
b.OnKill(w.ProcessName, w.Title)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Blocker) Start() {
|
||||
b.mu.Lock()
|
||||
if b.running {
|
||||
b.mu.Unlock()
|
||||
return
|
||||
}
|
||||
b.running = true
|
||||
b.stopChan = make(chan struct{})
|
||||
b.mu.Unlock()
|
||||
|
||||
go func() {
|
||||
ticker := time.NewTicker(3 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ticker.C:
|
||||
b.checkAndKill()
|
||||
case <-b.stopChan:
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
log.Println("[BLOCKER] Application Blocker Daemon Started.")
|
||||
}
|
||||
|
||||
func (b *Blocker) Stop() {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
|
||||
if !b.running {
|
||||
return
|
||||
}
|
||||
b.running = false
|
||||
close(b.stopChan)
|
||||
log.Println("[BLOCKER] Application Blocker Daemon Stopped.")
|
||||
}
|
||||
35
client/internal/screen/screen.go
Normal file
35
client/internal/screen/screen.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package screen
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"image/jpeg"
|
||||
|
||||
"github.com/kbinani/screenshot"
|
||||
)
|
||||
|
||||
// CaptureScreen chụp màn hình chính và trả về chuỗi Base64 dạng "data:image/jpeg;base64,..."
|
||||
func CaptureScreen() (string, error) {
|
||||
n := screenshot.NumActiveDisplays()
|
||||
if n <= 0 {
|
||||
return "", fmt.Errorf("no active displays found")
|
||||
}
|
||||
|
||||
// Chụp màn hình chính (index 0)
|
||||
bounds := screenshot.GetDisplayBounds(0)
|
||||
img, err := screenshot.CaptureRect(bounds)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to capture screen: %w", err)
|
||||
}
|
||||
|
||||
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})
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode jpeg: %w", err)
|
||||
}
|
||||
|
||||
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
|
||||
return "data:image/jpeg;base64," + encoded, nil
|
||||
}
|
||||
82
client/internal/winapi/winapi.go
Normal file
82
client/internal/winapi/winapi.go
Normal file
@@ -0,0 +1,82 @@
|
||||
package winapi
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os/exec"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// WifiConnection — SSID + BSSID (MAC) của điểm phát đang kết nối
|
||||
type WifiConnection struct {
|
||||
SSID string
|
||||
BSSID string
|
||||
}
|
||||
|
||||
// GetWifiSSID trả về SSID Wifi đang kết nối
|
||||
func GetWifiSSID() string {
|
||||
return GetWifiConnection().SSID
|
||||
}
|
||||
|
||||
// GetWifiConnection đọc SSID và BSSID từ netsh (Windows)
|
||||
func GetWifiConnection() WifiConnection {
|
||||
cmd := exec.Command("netsh", "wlan", "show", "interfaces")
|
||||
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
|
||||
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return WifiConnection{}
|
||||
}
|
||||
|
||||
var conn WifiConnection
|
||||
for _, line := range strings.Split(out.String(), "\n") {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
lower := strings.ToLower(trimmed)
|
||||
if strings.HasPrefix(lower, "ssid") && !strings.Contains(lower, "bssid") {
|
||||
if v := valueAfterColon(trimmed); v != "" {
|
||||
conn.SSID = v
|
||||
}
|
||||
}
|
||||
// Windows: "AP BSSID" (EN) hoặc dòng có chứa "bssid" (locale khác)
|
||||
if strings.Contains(lower, "bssid") {
|
||||
if v := valueAfterColon(trimmed); v != "" {
|
||||
conn.BSSID = normalizeMAC(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return conn
|
||||
}
|
||||
|
||||
func valueAfterColon(line string) string {
|
||||
idx := strings.Index(line, ":")
|
||||
if idx < 0 {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(line[idx+1:])
|
||||
}
|
||||
|
||||
func normalizeMAC(mac string) string {
|
||||
var hex []rune
|
||||
for _, c := range strings.ToLower(mac) {
|
||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
||||
hex = append(hex, c)
|
||||
}
|
||||
}
|
||||
if len(hex) != 12 {
|
||||
return strings.TrimSpace(mac)
|
||||
}
|
||||
return string(hex[0:2]) + ":" + string(hex[2:4]) + ":" + string(hex[4:6]) + ":" +
|
||||
string(hex[6:8]) + ":" + string(hex[8:10]) + ":" + string(hex[10:12])
|
||||
}
|
||||
|
||||
// BSSIDKey chuẩn hóa MAC để so khớp (12 ký tự hex)
|
||||
func BSSIDKey(bssid string) string {
|
||||
var hex []rune
|
||||
for _, c := range strings.ToLower(bssid) {
|
||||
if (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') {
|
||||
hex = append(hex, c)
|
||||
}
|
||||
}
|
||||
return string(hex)
|
||||
}
|
||||
36
client/main.go
Normal file
36
client/main.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// Create an instance of the app structure
|
||||
app := NewApp()
|
||||
|
||||
// Create application with options
|
||||
err := wails.Run(&options.App{
|
||||
Title: "Simple Care — Rikkei Education",
|
||||
Width: 1024,
|
||||
Height: 768,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||
OnStartup: app.startup,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
println("Error:", err.Error())
|
||||
}
|
||||
}
|
||||
13
client/wails.json
Normal file
13
client/wails.json
Normal file
@@ -0,0 +1,13 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "Simple Care by Rikkei Edu",
|
||||
"outputfilename": "simple_care_v1.0",
|
||||
"frontend:install": "npm install",
|
||||
"frontend:build": "npm run build",
|
||||
"frontend:dev:watcher": "npm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"author": {
|
||||
"name": "PhuocNTB",
|
||||
"email": "phuocntb@mieusoft.com"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user