diff --git a/cmd/vrc_osc_gui/history_view_windows.go b/cmd/vrc_osc_gui/history_view_windows.go index 27d11db..4c17323 100644 --- a/cmd/vrc_osc_gui/history_view_windows.go +++ b/cmd/vrc_osc_gui/history_view_windows.go @@ -89,6 +89,22 @@ type historyExportResult struct { SelectedCount int } +type historyRefreshResult struct { + Rows []guiWorldVisitRow + CalendarCells []historyCalendarCell + DetailRows []historyDetailRow + DetailHeightPx int32 + SelectedDate time.Time + Month time.Time + SelectedKeys map[string]bool + Status string + Regex string + FromDate string + ToDate string + Err error + Elapsed time.Duration +} + var historyEventsCacheMu sync.Mutex var historyEventsCache struct { jsonMod time.Time @@ -106,11 +122,7 @@ func ensureHistoryState(app *guiApp) { app.historySelectedKeys = map[string]bool{} } if app.historySelectedDate.IsZero() { - if latest := latestHistoryDate(); !latest.IsZero() { - app.historySelectedDate = latest - } else { - app.historySelectedDate = time.Now() - } + app.historySelectedDate = time.Now() } if app.historyMonth.IsZero() { app.historyMonth = time.Date(app.historySelectedDate.Year(), app.historySelectedDate.Month(), 1, 0, 0, 0, 0, app.historySelectedDate.Location()) @@ -757,6 +769,124 @@ func refreshHistoryCache(app *guiApp, force bool) bool { return true } +func startHistoryRefresh(app *guiApp, force bool) bool { + if app == nil || app.historyRefreshRunning { + return false + } + if !force && app.historyCacheLoaded && !app.historyReloadPending { + return false + } + ensureHistoryState(app) + if app.historyRefreshResultCh == nil { + app.historyRefreshResultCh = make(chan historyRefreshResult, 1) + } + work := *app + work.historySelectedKeys = cloneHistorySelection(app.historySelectedKeys) + work.historyRefreshResultCh = nil + work.historyExportResultCh = nil + app.historyRefreshRunning = true + app.historyReloadPending = true + if strings.TrimSpace(app.historyStatus) == "" { + app.historyStatus = "reloading..." + } + hwnd := app.hwnd + go func(ch chan<- historyRefreshResult, work guiApp, hwnd uintptr) { + start := time.Now() + result := historyRefreshResult{ + SelectedDate: work.historySelectedDate, + Month: work.historyMonth, + SelectedKeys: cloneHistorySelection(work.historySelectedKeys), + Regex: work.historyRegex, + FromDate: work.historyFromDate, + ToDate: work.historyToDate, + } + defer func() { + result.Elapsed = time.Since(start) + if r := recover(); r != nil { + result.Err = fmt.Errorf("refresh panic: %v", r) + } + select { + case ch <- result: + default: + } + if postMessageProc != nil && hwnd != 0 { + postMessageProc.Call(hwnd, uintptr(wmHistoryRefreshDone), 0, 0) + } + }() + refreshHistoryCache(&work, true) + result.Rows = append([]guiWorldVisitRow(nil), work.historyRows...) + result.CalendarCells = append([]historyCalendarCell(nil), work.historyCalendarCells...) + result.DetailRows = cloneHistoryDetailRows(work.historyDetailRows) + result.DetailHeightPx = work.historyDetailContentHeightPx + result.SelectedDate = work.historySelectedDate + result.Month = work.historyMonth + result.SelectedKeys = cloneHistorySelection(work.historySelectedKeys) + result.Status = work.historyStatus + }(app.historyRefreshResultCh, work, hwnd) + guiLog("history refresh start") + return true +} + +func drainHistoryRefreshResult(app *guiApp) bool { + if app == nil || app.historyRefreshResultCh == nil || !app.historyRefreshRunning { + return false + } + handled := false + for { + select { + case res := <-app.historyRefreshResultCh: + handled = true + app.historyRefreshRunning = false + if res.Err != nil { + app.historyStatus = "reload failed: " + res.Err.Error() + app.historyReloadPending = true + guiLog(fmt.Sprintf("history refresh failed elapsed=%s err=%v", res.Elapsed, res.Err)) + invalidateHistoryPanes(app) + continue + } + stale := !sameDay(app.historySelectedDate, res.SelectedDate) || + !sameHistoryMonth(app.historyMonth, res.Month) || + app.historyRegex != res.Regex || + app.historyFromDate != res.FromDate || + app.historyToDate != res.ToDate + if stale { + app.historyReloadPending = true + guiLog("history refresh stale; scheduling another refresh") + invalidateHistoryPanes(app) + continue + } + app.historyRows = res.Rows + app.historyCalendarCells = res.CalendarCells + app.historyDetailRows = res.DetailRows + app.historyDetailContentHeightPx = res.DetailHeightPx + app.historySelectedDate = res.SelectedDate + app.historyMonth = res.Month + app.historySelectedKeys = res.SelectedKeys + if app.historySelectedKeys == nil { + app.historySelectedKeys = map[string]bool{} + } + app.historyReloadPending = false + app.historyCacheLoaded = true + if strings.TrimSpace(res.Status) != "" { + app.historyStatus = res.Status + } else { + app.historyStatus = "reloaded: " + time.Now().Format("15:04:05") + } + guiLog(fmt.Sprintf("history refresh ok elapsed=%s rows=%d details=%d", res.Elapsed, len(res.Rows), len(res.DetailRows))) + invalidateHistoryPanes(app) + default: + return handled + } + } +} + +func sameHistoryMonth(a, b time.Time) bool { + if a.IsZero() || b.IsZero() { + return a.IsZero() && b.IsZero() + } + return a.Year() == b.Year() && a.Month() == b.Month() +} + func requestHistoryRefresh(app *guiApp, reason string) { if app == nil { return diff --git a/cmd/vrc_osc_gui/main.go b/cmd/vrc_osc_gui/main.go index 7025dff..e80594d 100644 --- a/cmd/vrc_osc_gui/main.go +++ b/cmd/vrc_osc_gui/main.go @@ -56,6 +56,8 @@ func normalizeInitialTab(tab string) string { switch strings.ToLower(strings.TrimSpace(tab)) { case "join", "log", "logs": return "join" + case "history", "hist": + return "history" case "translate", "translation": return "join" case "settings", "setting", "config": diff --git a/cmd/vrc_osc_gui/window_windows.go b/cmd/vrc_osc_gui/window_windows.go index 5d5cc50..a6abbbd 100644 --- a/cmd/vrc_osc_gui/window_windows.go +++ b/cmd/vrc_osc_gui/window_windows.go @@ -52,6 +52,7 @@ const ( wmCommandGUI = 0x0111 wmSetFont = 0x0030 wmHistoryExportDone = 0x0401 + wmHistoryRefreshDone = 0x0402 enChange = 0x0300 enKillFocus = 0x0200 bnClicked = 0x0000 @@ -251,6 +252,8 @@ type guiApp struct { historyReloadRect winRect historyExportRect winRect historyReloadPending bool + historyRefreshRunning bool + historyRefreshResultCh chan historyRefreshResult historyCacheLoaded bool historyDetailContentHeightPx int32 settingsExportDirEdit uintptr @@ -466,7 +469,6 @@ func runNativeGUI(initialTab string) error { app.historyExportCustom = strings.TrimSpace(guiCfg.HistoryExportCustom) app.historyReloadPending = true app.hFont = createAppFont(app.fontSize) - refreshHistoryCache(&app, true) paneWndProc := syscall.NewCallback(func(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr { defer func() { if r := recover(); r != nil { @@ -851,17 +853,31 @@ func runNativeGUI(initialTab string) error { } case wmTimerGUI: drainHistoryExportResult(&app) + drainHistoryRefreshResult(&app) reloadData := app.activeTab == "join" || app.activeTab == "translate" if app.activeTab == "history" && app.historyReloadPending { - if refreshHistoryCache(&app, true) { - refreshGUI(setWindowText, &app, false) - } + startHistoryRefresh(&app, true) + refreshGUI(setWindowText, &app, false) refreshSettingsControls(showWindowProc, setWindowPosProc, &app) return 0 } refreshGUI(setWindowText, &app, reloadData) refreshSettingsControls(showWindowProc, setWindowPosProc, &app) return 0 + case wmHistoryRefreshDone: + drainHistoryRefreshResult(&app) + if invalidateRectProc != nil { + if app.leftHwnd != 0 { + invalidateRectProc.Call(app.leftHwnd, 0, 1) + } + if app.rightHwnd != 0 { + invalidateRectProc.Call(app.rightHwnd, 0, 1) + } + if app.hwnd != 0 { + invalidateRectProc.Call(app.hwnd, 0, 1) + } + } + return 0 case wmHistoryExportDone: drainHistoryExportResult(&app) if invalidateRectProc != nil { @@ -984,11 +1000,7 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) { instanceCount := app.currentUserCount worldLabel := app.currentWorld state := readRuntimeSnapshot() - historyDirty := false - historyNeedsRefresh := app.activeTab == "history" && (app.historyReloadPending || !app.historyCacheLoaded) - if historyNeedsRefresh { - historyDirty = refreshHistoryCache(app, false) - } + historyDirty := app.activeTab == "history" && app.historyReloadPending if reloadData { if app.activeTab == "translate" { guiLog("refreshGUI activeTab=translate light refresh")