Clean project layout and fix release workflow
Some checks failed
build-windows-exe / build (push) Failing after 1m13s
Some checks failed
build-windows-exe / build (push) Failing after 1m13s
This commit is contained in:
@@ -3,12 +3,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"unsafe"
|
||||
@@ -59,6 +61,43 @@ type historyQuery struct {
|
||||
HasTo bool
|
||||
}
|
||||
|
||||
type joinLeaveEventSnapshot struct {
|
||||
At time.Time
|
||||
Kind string
|
||||
Name string
|
||||
Count int
|
||||
}
|
||||
|
||||
type historyExportRequest struct {
|
||||
Day time.Time
|
||||
Selected map[string]bool
|
||||
Rows []historyDetailRow
|
||||
ExportDir string
|
||||
IncludeTime bool
|
||||
IncludeWorld bool
|
||||
IncludeJoinLeave bool
|
||||
CustomEnabled bool
|
||||
Custom string
|
||||
RegexText string
|
||||
Query historyQuery
|
||||
}
|
||||
|
||||
type historyExportResult struct {
|
||||
Path string
|
||||
Err error
|
||||
Day time.Time
|
||||
SelectedCount int
|
||||
}
|
||||
|
||||
var historyEventsCacheMu sync.Mutex
|
||||
var historyEventsCache struct {
|
||||
jsonMod time.Time
|
||||
jsonSize int64
|
||||
logMod time.Time
|
||||
logSize int64
|
||||
items []historyEventRecord
|
||||
}
|
||||
|
||||
func ensureHistoryState(app *guiApp) {
|
||||
if app == nil {
|
||||
return
|
||||
@@ -221,9 +260,37 @@ func historyVisitKey(label string, start, end time.Time, worldID, instanceID str
|
||||
return key
|
||||
}
|
||||
|
||||
func historyEventsFromLog() []historyEventRecord {
|
||||
p := filepath.Join(runtimeDir(), "join_leave.log")
|
||||
b, err := os.ReadFile(p)
|
||||
func historyEventsFromSnapshot() []historyEventRecord {
|
||||
jsonPath := filepath.Join(runtimeDir(), "join_leave_events.json")
|
||||
logPath := filepath.Join(runtimeDir(), "join_leave.log")
|
||||
jsonInfo, jsonErr := os.Stat(jsonPath)
|
||||
logInfo, logErr := os.Stat(logPath)
|
||||
if jsonErr == nil || logErr == nil {
|
||||
historyEventsCacheMu.Lock()
|
||||
if historyEventsCache.items != nil &&
|
||||
((jsonErr == nil && historyEventsCache.jsonMod.Equal(jsonInfo.ModTime()) && historyEventsCache.jsonSize == jsonInfo.Size()) || jsonErr != nil) &&
|
||||
((logErr == nil && historyEventsCache.logMod.Equal(logInfo.ModTime()) && historyEventsCache.logSize == logInfo.Size()) || logErr != nil) {
|
||||
out := append([]historyEventRecord(nil), historyEventsCache.items...)
|
||||
historyEventsCacheMu.Unlock()
|
||||
return out
|
||||
}
|
||||
historyEventsCacheMu.Unlock()
|
||||
}
|
||||
if b, err := os.ReadFile(jsonPath); err == nil && len(b) > 0 {
|
||||
if events := decodeHistoryEventsSnapshot(b); len(events) > 0 {
|
||||
if jsonErr == nil {
|
||||
historyEventsCacheMu.Lock()
|
||||
historyEventsCache.jsonMod = jsonInfo.ModTime()
|
||||
historyEventsCache.jsonSize = jsonInfo.Size()
|
||||
historyEventsCache.logMod = time.Time{}
|
||||
historyEventsCache.logSize = 0
|
||||
historyEventsCache.items = append([]historyEventRecord(nil), events...)
|
||||
historyEventsCacheMu.Unlock()
|
||||
}
|
||||
return events
|
||||
}
|
||||
}
|
||||
b, err := os.ReadFile(logPath)
|
||||
if err != nil || len(b) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -251,6 +318,76 @@ func historyEventsFromLog() []historyEventRecord {
|
||||
Raw: line,
|
||||
})
|
||||
}
|
||||
if logErr == nil {
|
||||
historyEventsCacheMu.Lock()
|
||||
historyEventsCache.jsonMod = time.Time{}
|
||||
historyEventsCache.jsonSize = 0
|
||||
historyEventsCache.logMod = logInfo.ModTime()
|
||||
historyEventsCache.logSize = logInfo.Size()
|
||||
historyEventsCache.items = append([]historyEventRecord(nil), out...)
|
||||
historyEventsCacheMu.Unlock()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeHistoryEventsSnapshot(b []byte) []historyEventRecord {
|
||||
type snapshot struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
Events []struct {
|
||||
At time.Time `json:"at"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
} `json:"events"`
|
||||
}
|
||||
var snap snapshot
|
||||
if err := json.Unmarshal(b, &snap); err == nil && len(snap.Events) > 0 {
|
||||
out := make([]historyEventRecord, 0, len(snap.Events))
|
||||
for _, ev := range snap.Events {
|
||||
out = append(out, historyEventRecord{
|
||||
At: ev.At,
|
||||
Kind: ev.Kind,
|
||||
Name: ev.Name,
|
||||
Raw: fmt.Sprintf("[%s] %s %s", ev.At.Format("15:04"), ev.Kind, ev.Name),
|
||||
})
|
||||
}
|
||||
return dedupeHistoryEventRecords(out)
|
||||
}
|
||||
var flat []struct {
|
||||
At time.Time `json:"at"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
if err := json.Unmarshal(b, &flat); err == nil && len(flat) > 0 {
|
||||
out := make([]historyEventRecord, 0, len(flat))
|
||||
for _, ev := range flat {
|
||||
out = append(out, historyEventRecord{
|
||||
At: ev.At,
|
||||
Kind: ev.Kind,
|
||||
Name: ev.Name,
|
||||
Raw: fmt.Sprintf("[%s] %s %s", ev.At.Format("15:04"), ev.Kind, ev.Name),
|
||||
})
|
||||
}
|
||||
return dedupeHistoryEventRecords(out)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func dedupeHistoryEventRecords(events []historyEventRecord) []historyEventRecord {
|
||||
if len(events) <= 1 {
|
||||
return events
|
||||
}
|
||||
seen := make(map[string]struct{}, len(events))
|
||||
out := make([]historyEventRecord, 0, len(events))
|
||||
for _, ev := range events {
|
||||
key := ev.At.UTC().Format(time.RFC3339Nano) + "|" + ev.Kind + "|" + strings.TrimSpace(ev.Name)
|
||||
if _, ok := seen[key]; ok {
|
||||
continue
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
out = append(out, ev)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -259,9 +396,13 @@ func historyVisitsForDay(app *guiApp, day time.Time) []historyDetailRow {
|
||||
if !historyInQueryRange(day, q) {
|
||||
return nil
|
||||
}
|
||||
dayStart, dayEnd := historyDayBounds(day)
|
||||
events := historyEventsFromLog()
|
||||
events := historyEventsFromSnapshot()
|
||||
visits := historyVisitSource()
|
||||
return historyVisitsForDayFromData(app, day, visits, events, q)
|
||||
}
|
||||
|
||||
func historyVisitsForDayFromData(app *guiApp, day time.Time, visits []historyVisitRecord, events []historyEventRecord, q historyQuery) []historyDetailRow {
|
||||
dayStart, dayEnd := historyDayBounds(day)
|
||||
rows := make([]historyDetailRow, 0, len(visits))
|
||||
for _, visit := range visits {
|
||||
visitStart := visit.Start
|
||||
@@ -349,11 +490,45 @@ func historyCalendarCellsForMonth(app *guiApp) []historyCalendarCell {
|
||||
monthStart = time.Now()
|
||||
}
|
||||
monthStart = time.Date(monthStart.Year(), monthStart.Month(), 1, 0, 0, 0, 0, monthStart.Location())
|
||||
events := historyEventsFromSnapshot()
|
||||
visits := historyVisitSource()
|
||||
return historyCalendarCellsForMonthFromData(app, monthStart, visits, events, q)
|
||||
}
|
||||
|
||||
func historyCalendarCellsForMonthFromData(app *guiApp, monthStart time.Time, visits []historyVisitRecord, events []historyEventRecord, q historyQuery) []historyCalendarCell {
|
||||
counts := map[string]int{}
|
||||
daysInMonth := monthStart.AddDate(0, 1, -1).Day()
|
||||
for day := 1; day <= daysInMonth; day++ {
|
||||
date := time.Date(monthStart.Year(), monthStart.Month(), day, 0, 0, 0, 0, monthStart.Location())
|
||||
counts[date.Format("2006-01-02")] = len(historyVisitsForDay(app, date))
|
||||
if q.Pattern == nil {
|
||||
monthEnd := monthStart.AddDate(0, 1, 0)
|
||||
for _, visit := range visits {
|
||||
visitStart := visit.Start
|
||||
if visitStart.IsZero() {
|
||||
continue
|
||||
}
|
||||
visitEnd := visit.End
|
||||
if visitEnd.IsZero() {
|
||||
visitEnd = time.Now()
|
||||
}
|
||||
if !visitStart.Before(monthEnd) || !visitEnd.After(monthStart) {
|
||||
continue
|
||||
}
|
||||
dayCursor := time.Date(visitStart.Year(), visitStart.Month(), visitStart.Day(), 0, 0, 0, 0, monthStart.Location())
|
||||
if dayCursor.Before(monthStart) {
|
||||
dayCursor = monthStart
|
||||
}
|
||||
for dayCursor.Before(monthEnd) {
|
||||
dayStart, dayEnd := historyDayBounds(dayCursor)
|
||||
if visitStart.Before(dayEnd) && visitEnd.After(dayStart) {
|
||||
counts[dayCursor.Format("2006-01-02")]++
|
||||
}
|
||||
dayCursor = dayCursor.AddDate(0, 0, 1)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for day := 1; day <= daysInMonth; day++ {
|
||||
date := time.Date(monthStart.Year(), monthStart.Month(), day, 0, 0, 0, 0, monthStart.Location())
|
||||
counts[date.Format("2006-01-02")] = len(historyVisitsForDayFromData(app, date, visits, events, q))
|
||||
}
|
||||
}
|
||||
firstWeekday := int(monthStart.Weekday())
|
||||
out := make([]historyCalendarCell, 0, 42)
|
||||
@@ -388,57 +563,119 @@ func sameDay(a, b time.Time) bool {
|
||||
}
|
||||
|
||||
func exportHistoryDay(app *guiApp, day time.Time, selected map[string]bool) (string, error) {
|
||||
rows := historyVisitsForDay(app, day)
|
||||
if len(rows) == 0 {
|
||||
return "", nil
|
||||
req := historyExportRequest{Day: day, Query: historyQueryFromApp(app)}
|
||||
if app != nil {
|
||||
req.Selected = cloneHistorySelection(selected)
|
||||
req.Rows = cloneHistoryDetailRows(app.historyDetailRows)
|
||||
req.ExportDir = normalizeHistoryExportDir(app.historyExportDir)
|
||||
req.IncludeTime = app.historyExportIncludeTime
|
||||
req.IncludeWorld = app.historyExportIncludeWorld
|
||||
req.IncludeJoinLeave = app.historyExportIncludeJoinLeave
|
||||
req.CustomEnabled = app.historyExportCustomEnabled
|
||||
req.Custom = strings.TrimSpace(app.historyExportCustom)
|
||||
req.RegexText = strings.TrimSpace(app.historyRegex)
|
||||
}
|
||||
if len(selected) > 0 {
|
||||
return exportHistoryDayFromRequest(req)
|
||||
}
|
||||
|
||||
func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
|
||||
rows := append([]historyDetailRow(nil), req.Rows...)
|
||||
if len(rows) == 0 {
|
||||
rows = historyVisitsForDayFromData(nil, req.Day, historyVisitSource(), historyEventsFromSnapshot(), req.Query)
|
||||
}
|
||||
if len(req.Selected) > 0 {
|
||||
filtered := rows[:0]
|
||||
for _, row := range rows {
|
||||
if selected[row.Key] {
|
||||
if req.Selected[row.Key] {
|
||||
filtered = append(filtered, row)
|
||||
}
|
||||
}
|
||||
rows = append([]historyDetailRow(nil), filtered...)
|
||||
}
|
||||
if len(rows) == 0 {
|
||||
return "", nil
|
||||
customPattern := (*regexp.Regexp)(nil)
|
||||
if req.CustomEnabled {
|
||||
if s := strings.TrimSpace(req.Custom); s != "" {
|
||||
if re, err := regexp.Compile(s); err == nil {
|
||||
customPattern = re
|
||||
}
|
||||
}
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("Date: ")
|
||||
b.WriteString(day.Format("2006-01-02"))
|
||||
b.WriteString(req.Day.Format("2006-01-02"))
|
||||
b.WriteString("\n")
|
||||
if q := historyQueryFromApp(app); q.Pattern != nil {
|
||||
if len(req.Selected) > 0 {
|
||||
b.WriteString("Selection: ")
|
||||
b.WriteString(fmt.Sprintf("%d rows", len(req.Selected)))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if req.Query.Pattern != nil {
|
||||
b.WriteString("Filter: ")
|
||||
b.WriteString(strings.TrimSpace(app.historyRegex))
|
||||
if strings.TrimSpace(req.RegexText) != "" {
|
||||
b.WriteString(strings.TrimSpace(req.RegexText))
|
||||
} else {
|
||||
b.WriteString(req.Query.Pattern.String())
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if req.CustomEnabled && customPattern != nil {
|
||||
b.WriteString("Custom: ")
|
||||
b.WriteString(strings.TrimSpace(req.Custom))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
if len(rows) == 0 {
|
||||
if len(req.Selected) > 0 {
|
||||
b.WriteString("(no selected visits)\n")
|
||||
} else {
|
||||
b.WriteString("(no visits)\n")
|
||||
}
|
||||
}
|
||||
for _, row := range rows {
|
||||
b.WriteString("\nWorld: ")
|
||||
b.WriteString(strings.TrimSpace(row.WorldLabel))
|
||||
b.WriteString("\n")
|
||||
b.WriteString("Time: ")
|
||||
b.WriteString(row.Start.Format("15:04"))
|
||||
b.WriteString(" - ")
|
||||
b.WriteString(row.End.Format("15:04"))
|
||||
b.WriteString("\n")
|
||||
rowLines := append([]string(nil), row.Lines...)
|
||||
if customPattern != nil {
|
||||
filtered := rowLines[:0]
|
||||
for _, line := range rowLines {
|
||||
if customPattern.MatchString(line) || customPattern.MatchString(row.WorldLabel) || customPattern.MatchString(strings.Join(row.Users, " ")) {
|
||||
filtered = append(filtered, line)
|
||||
}
|
||||
}
|
||||
rowLines = append([]string(nil), filtered...)
|
||||
}
|
||||
if req.IncludeWorld {
|
||||
b.WriteString("\nWorld: ")
|
||||
b.WriteString(strings.TrimSpace(row.WorldLabel))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if req.IncludeTime {
|
||||
b.WriteString("Time: ")
|
||||
b.WriteString(historyTimeRangeLabel(row.Start, row.End, row.Current))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if len(row.Users) > 0 {
|
||||
b.WriteString("Users: ")
|
||||
b.WriteString(strings.Join(row.Users, ", "))
|
||||
b.WriteString("\n")
|
||||
}
|
||||
if len(row.Lines) == 0 {
|
||||
b.WriteString("(no matches)\n")
|
||||
continue
|
||||
}
|
||||
for _, line := range row.Lines {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
if req.IncludeJoinLeave {
|
||||
if len(rowLines) == 0 {
|
||||
b.WriteString("(no matches)\n")
|
||||
}
|
||||
for _, line := range rowLines {
|
||||
b.WriteString(line)
|
||||
b.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
path := filepath.Join(runtimeDir(), "visit_"+day.Format("20060102")+".log")
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
||||
exportDir := runtimeDir()
|
||||
if strings.TrimSpace(req.ExportDir) != "" {
|
||||
exportDir = normalizeHistoryExportDir(req.ExportDir)
|
||||
if !filepath.IsAbs(exportDir) {
|
||||
exportDir = filepath.Join(runtimeDir(), exportDir)
|
||||
}
|
||||
}
|
||||
path := filepath.Join(exportDir, "visit_"+req.Day.Format("20060102")+".log")
|
||||
if err := os.MkdirAll(exportDir, 0o755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := os.WriteFile(path, []byte(b.String()), 0o644); err != nil {
|
||||
@@ -447,6 +684,89 @@ func exportHistoryDay(app *guiApp, day time.Time, selected map[string]bool) (str
|
||||
return path, nil
|
||||
}
|
||||
|
||||
func cloneHistorySelection(selected map[string]bool) map[string]bool {
|
||||
if len(selected) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make(map[string]bool, len(selected))
|
||||
for key, ok := range selected {
|
||||
if ok {
|
||||
out[key] = true
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func cloneHistoryDetailRows(rows []historyDetailRow) []historyDetailRow {
|
||||
if len(rows) == 0 {
|
||||
return nil
|
||||
}
|
||||
out := make([]historyDetailRow, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
cloned := row
|
||||
if len(row.Users) > 0 {
|
||||
cloned.Users = append([]string(nil), row.Users...)
|
||||
}
|
||||
if len(row.Lines) > 0 {
|
||||
cloned.Lines = append([]string(nil), row.Lines...)
|
||||
}
|
||||
out = append(out, cloned)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func refreshHistoryCache(app *guiApp, force bool) bool {
|
||||
if app == nil {
|
||||
return false
|
||||
}
|
||||
ensureHistoryState(app)
|
||||
if !force && app.historyCacheLoaded && !app.historyReloadPending {
|
||||
return false
|
||||
}
|
||||
events := historyEventsFromSnapshot()
|
||||
visits := historyVisitSource()
|
||||
q := historyQueryFromApp(app)
|
||||
app.historyRows = historyRows()
|
||||
if app.historyRows == nil {
|
||||
app.historyRows = []guiWorldVisitRow{}
|
||||
}
|
||||
monthStart := app.historyMonth
|
||||
if monthStart.IsZero() {
|
||||
monthStart = time.Now()
|
||||
}
|
||||
monthStart = time.Date(monthStart.Year(), monthStart.Month(), 1, 0, 0, 0, 0, monthStart.Location())
|
||||
app.historyCalendarCells = historyCalendarCellsForMonthFromData(app, monthStart, visits, events, q)
|
||||
if app.historyCalendarCells == nil {
|
||||
app.historyCalendarCells = []historyCalendarCell{}
|
||||
}
|
||||
app.historyDetailRows = historyVisitsForDayFromData(app, app.historySelectedDate, visits, events, q)
|
||||
if app.historyDetailRows == nil {
|
||||
app.historyDetailRows = []historyDetailRow{}
|
||||
}
|
||||
height := int32(132)
|
||||
for _, row := range app.historyDetailRows {
|
||||
height += historyDetailRowHeight(row) + 6
|
||||
}
|
||||
if height < 120 {
|
||||
height = 120
|
||||
}
|
||||
app.historyDetailContentHeightPx = height
|
||||
app.historyReloadPending = false
|
||||
app.historyCacheLoaded = true
|
||||
app.historyStatus = "reloaded: " + time.Now().Format("15:04:05")
|
||||
return true
|
||||
}
|
||||
|
||||
func requestHistoryRefresh(app *guiApp, reason string) {
|
||||
if app == nil {
|
||||
return
|
||||
}
|
||||
app.historyReloadPending = true
|
||||
if strings.TrimSpace(reason) != "" {
|
||||
app.historyStatus = reason
|
||||
}
|
||||
}
|
||||
|
||||
func paintHistoryCalendarPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
if app == nil {
|
||||
return 0
|
||||
@@ -479,14 +799,17 @@ func paintHistoryCalendarPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
}
|
||||
cellH := int32(56)
|
||||
startX := int32(12)
|
||||
startY := int32(80)
|
||||
startY := int32(102)
|
||||
for i, wd := range weekdays {
|
||||
x := startX + int32(i)*cellW
|
||||
drawPaneText(hdc, x+6, startY-18, wd, app.hFont, clrMutedText)
|
||||
}
|
||||
|
||||
cells := historyCalendarCellsForMonth(app)
|
||||
app.historyCalendarCells = app.historyCalendarCells[:0]
|
||||
cells := app.historyCalendarCells
|
||||
if len(cells) == 0 {
|
||||
drawPaneText(hdc, 24, 118, "履歴カレンダーを読み込み中", app.joinBoldHFont, clrMutedText)
|
||||
drawPaneText(hdc, 24, 146, "しばらくしても出ない場合は再読み込みを押してください", app.hFont, clrMutedText)
|
||||
}
|
||||
for i, cell := range cells {
|
||||
if cell.Date.IsZero() {
|
||||
continue
|
||||
@@ -500,7 +823,7 @@ func paintHistoryCalendarPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
Bottom: startY + int32(row+1)*cellH - 4,
|
||||
}
|
||||
cell.Rect = rect
|
||||
app.historyCalendarCells = append(app.historyCalendarCells, cell)
|
||||
app.historyCalendarCells[i].Rect = rect
|
||||
fill := uint32(0x00261a11)
|
||||
border := uint32(clrPaneBorder)
|
||||
textColor := uint32(clrText)
|
||||
@@ -533,6 +856,12 @@ func paintHistoryDetailPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
if app == nil {
|
||||
return 0
|
||||
}
|
||||
start := time.Now()
|
||||
defer func() {
|
||||
if d := time.Since(start); d > 80*time.Millisecond {
|
||||
guiLog(fmt.Sprintf("paintHistoryDetailPane slow=%s rows=%d scroll=%d", d, len(app.historyDetailRows), app.historyScrollPos))
|
||||
}
|
||||
}()
|
||||
ensureHistoryState(app)
|
||||
hdc, done := beginPanePaint(hwnd)
|
||||
if hdc == 0 {
|
||||
@@ -544,15 +873,14 @@ func paintHistoryDetailPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
getClientRectProc.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
|
||||
fillSolid(hdc, rc, clrPaneBgAlt)
|
||||
|
||||
rows := historyVisitsForDay(app, app.historySelectedDate)
|
||||
app.historyDetailRows = app.historyDetailRows[:0]
|
||||
rows := app.historyDetailRows
|
||||
|
||||
title := app.historySelectedDate.Format("2006年01月02日")
|
||||
drawPaneText(hdc, 18, 18, title, app.joinBoldHFont, clrAccentGreen)
|
||||
drawPaneText(hdc, 18, 52, title, app.joinBoldHFont, clrAccentGreen)
|
||||
summary := fmt.Sprintf("%d件", len(rows))
|
||||
drawPaneText(hdc, 18, 44, summary, app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, 18, 78, summary, app.hFont, clrMutedText)
|
||||
if strings.TrimSpace(app.historyRegex) != "" {
|
||||
drawPaneText(hdc, 96, 44, "grep: "+app.historyRegex, app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, 96, 78, "grep: "+app.historyRegex, app.hFont, clrMutedText)
|
||||
}
|
||||
if strings.TrimSpace(app.historyFromDate) != "" || strings.TrimSpace(app.historyToDate) != "" {
|
||||
rangeLabel := strings.TrimSpace(app.historyFromDate)
|
||||
@@ -565,10 +893,13 @@ func paintHistoryDetailPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
} else {
|
||||
rangeLabel += "..."
|
||||
}
|
||||
drawPaneText(hdc, 260, 44, rangeLabel, app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, 260, 78, rangeLabel, app.hFont, clrMutedText)
|
||||
}
|
||||
|
||||
app.historyExportRect = winRect{Left: rc.Right - 132, Top: 14, Right: rc.Right - 16, Bottom: 42}
|
||||
app.historyReloadRect = winRect{Left: rc.Right - 256, Top: 10, Right: rc.Right - 140, Bottom: 38}
|
||||
drawSettingsBox(hdc, app.historyReloadRect, 0x0038281a, clrPaneBorder)
|
||||
drawCenteredPaneText(hdc, app.historyReloadRect, "Reload", app.hFont, clrText)
|
||||
app.historyExportRect = winRect{Left: rc.Right - 132, Top: 10, Right: rc.Right - 16, Bottom: 38}
|
||||
drawSettingsBox(hdc, app.historyExportRect, 0x0038281a, clrAccentGreen)
|
||||
drawCenteredPaneText(hdc, app.historyExportRect, "Export", app.hFont, clrText)
|
||||
if app.historyStatus != "" {
|
||||
@@ -583,11 +914,11 @@ func paintHistoryDetailPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
}
|
||||
applyPaneScroll(hwnd, app.historyScrollPos, contentHeight, pageHeight)
|
||||
|
||||
y := int32(82 - app.historyScrollPos)
|
||||
for _, row := range rows {
|
||||
y := int32(126 - app.historyScrollPos)
|
||||
for i, row := range rows {
|
||||
rowHeight := historyDetailRowHeight(row)
|
||||
row.Rect = winRect{Left: 14, Top: y, Right: rc.Right - 14, Bottom: y + rowHeight - 4}
|
||||
app.historyDetailRows = append(app.historyDetailRows, row)
|
||||
app.historyDetailRows[i].Rect = row.Rect
|
||||
if row.Rect.Bottom < 72 {
|
||||
y += rowHeight
|
||||
continue
|
||||
@@ -605,8 +936,9 @@ func paintHistoryDetailPane(hwnd uintptr, app *guiApp) uintptr {
|
||||
border = clrAccentGreen
|
||||
}
|
||||
drawSettingsBox(hdc, row.Rect, fill, border)
|
||||
drawPaneText(hdc, row.Rect.Left+12, y+6, row.Start.Format("15:04")+" - "+row.End.Format("15:04"), app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, row.Rect.Left+122, y+6, humanDurationLabel(row.Start, row.End, row.Current), app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, row.Rect.Left+12, y+6, "入室 "+historyEntryTimeLabel(row.Start, false, row.Current), app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, row.Rect.Left+146, y+6, "退室 "+historyEntryTimeLabel(row.End, true, row.Current), app.hFont, clrMutedText)
|
||||
drawPaneText(hdc, row.Rect.Right-110, y+6, humanDurationLabel(row.Start, row.End, row.Current), app.hFont, clrMutedText)
|
||||
label := ellipsizeTextToWidth(hdc, row.WorldLabel, row.Rect.Right-row.Rect.Left-150, app.joinBoldHFont)
|
||||
if label == "" {
|
||||
label = "(unknown)"
|
||||
@@ -635,16 +967,18 @@ func historyDetailContentHeight(hwnd uintptr, app *guiApp) int32 {
|
||||
if app == nil || getClientRectProc == nil {
|
||||
return 0
|
||||
}
|
||||
var rc winRect
|
||||
getClientRectProc.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
|
||||
rows := historyVisitsForDay(app, app.historySelectedDate)
|
||||
height := int32(90)
|
||||
if app.historyDetailContentHeightPx > 0 {
|
||||
return app.historyDetailContentHeightPx
|
||||
}
|
||||
rows := app.historyDetailRows
|
||||
height := int32(132)
|
||||
for _, row := range rows {
|
||||
height += historyDetailRowHeight(row) + 6
|
||||
}
|
||||
if height < 120 {
|
||||
height = 120
|
||||
}
|
||||
app.historyDetailContentHeightPx = height
|
||||
return height
|
||||
}
|
||||
|
||||
@@ -666,7 +1000,55 @@ func historyDetailRowHeight(row historyDetailRow) int32 {
|
||||
return height
|
||||
}
|
||||
|
||||
func handleHistoryPaneClick(app *guiApp, hwnd uintptr, x, y int32, setWindowText *syscall.LazyProc) bool {
|
||||
func historyTimeRangeLabel(start, end time.Time, current bool) string {
|
||||
if start.IsZero() {
|
||||
return ""
|
||||
}
|
||||
if end.IsZero() {
|
||||
end = time.Now()
|
||||
}
|
||||
if end.Before(start) {
|
||||
end = start
|
||||
}
|
||||
startLabel := start.Format("15:04")
|
||||
endLabel := end.Format("15:04")
|
||||
if !sameDay(start, end) {
|
||||
startLabel = start.Format("01/02 15:04")
|
||||
if !current && isMidnight(end) {
|
||||
endLabel = "23:59"
|
||||
} else {
|
||||
endLabel = end.Format("01/02 15:04")
|
||||
}
|
||||
} else if !current && isMidnight(end) {
|
||||
endLabel = "23:59"
|
||||
}
|
||||
if current {
|
||||
return startLabel + " - now"
|
||||
}
|
||||
return startLabel + " - " + endLabel
|
||||
}
|
||||
|
||||
func isMidnight(t time.Time) bool {
|
||||
return t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0 && t.Nanosecond() == 0
|
||||
}
|
||||
|
||||
func historyEntryTimeLabel(t time.Time, isEnd bool, current bool) string {
|
||||
if t.IsZero() {
|
||||
if current {
|
||||
return "now"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
if current {
|
||||
return "now"
|
||||
}
|
||||
if isEnd && isMidnight(t) {
|
||||
return "23:59"
|
||||
}
|
||||
return t.Format("15:04")
|
||||
}
|
||||
|
||||
func handleHistoryPaneClick(app *guiApp, hwnd uintptr, x, y int32, getWindowText, setWindowText *syscall.LazyProc) bool {
|
||||
if app == nil {
|
||||
return false
|
||||
}
|
||||
@@ -689,18 +1071,16 @@ func handleHistoryPaneClick(app *guiApp, hwnd uintptr, x, y int32, setWindowText
|
||||
return true
|
||||
}
|
||||
case app.rightHwnd:
|
||||
if pointInRect(x, y, app.historyExportRect) {
|
||||
path, err := exportHistoryDay(app, app.historySelectedDate, app.historySelectedKeys)
|
||||
if err != nil {
|
||||
app.historyStatus = "export failed: " + err.Error()
|
||||
} else if path != "" {
|
||||
app.historyStatus = "exported: " + filepath.Base(path)
|
||||
} else {
|
||||
app.historyStatus = "exported: nothing selected"
|
||||
}
|
||||
if pointInRect(x, y, app.historyReloadRect) {
|
||||
requestHistoryRefresh(app, "reloading...")
|
||||
invalidateHistoryPanes(app)
|
||||
return true
|
||||
}
|
||||
if pointInRect(x, y, app.historyExportRect) {
|
||||
guiLog(fmt.Sprintf("history export click day=%s status=%q", app.historySelectedDate.Format("2006-01-02"), app.historyStatus))
|
||||
startHistoryExport(app, getWindowText, setWindowText)
|
||||
return true
|
||||
}
|
||||
for _, row := range app.historyDetailRows {
|
||||
if row.Key == "" || !pointInRect(x, y, row.Rect) {
|
||||
continue
|
||||
@@ -761,6 +1141,7 @@ func historySelectDate(app *guiApp, day time.Time) {
|
||||
app.historySelectedKeys = map[string]bool{}
|
||||
app.historyStatus = ""
|
||||
app.historyScrollPos = 0
|
||||
requestHistoryRefresh(app, "reloading...")
|
||||
invalidateHistoryPanes(app)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,22 +3,95 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"vrc_osc_go/internal/app"
|
||||
"vrc_osc_go/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
initialTab := parseInitialTab(os.Args[1:])
|
||||
if exe, err := os.Executable(); err == nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe exe="+exe+" dir="+filepath.Dir(exe))
|
||||
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe exe="+exe+" dir="+filepath.Dir(exe)+" initial_tab="+initialTab)
|
||||
} else {
|
||||
_ = app.AppendRuntimeLog("GUI", "starting vrc_osc_gui.exe exe=unknown err="+err.Error())
|
||||
}
|
||||
if err := runNativeGUI(); err != nil {
|
||||
ensureRuntimeStarted()
|
||||
if err := runNativeGUI(initialTab); err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "runNativeGUI failed: "+err.Error())
|
||||
log.Fatal(err)
|
||||
}
|
||||
_ = app.AppendRuntimeLog("GUI", "runNativeGUI returned cleanly")
|
||||
}
|
||||
|
||||
func parseInitialTab(args []string) string {
|
||||
for _, arg := range args {
|
||||
switch {
|
||||
case arg == "--settings" || arg == "-settings" || arg == "/settings":
|
||||
return "settings"
|
||||
case arg == "--translate" || arg == "-translate" || arg == "/translate":
|
||||
return "join"
|
||||
case arg == "--log" || arg == "-log" || arg == "/log":
|
||||
return "join"
|
||||
case strings.HasPrefix(arg, "--tab="):
|
||||
if tab := normalizeInitialTab(strings.TrimPrefix(arg, "--tab=")); tab != "" {
|
||||
return tab
|
||||
}
|
||||
case strings.HasPrefix(arg, "/tab="):
|
||||
if tab := normalizeInitialTab(strings.TrimPrefix(arg, "/tab=")); tab != "" {
|
||||
return tab
|
||||
}
|
||||
}
|
||||
}
|
||||
return "join"
|
||||
}
|
||||
|
||||
func normalizeInitialTab(tab string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(tab)) {
|
||||
case "join", "log", "logs":
|
||||
return "join"
|
||||
case "translate", "translation":
|
||||
return "join"
|
||||
case "settings", "setting", "config":
|
||||
return "settings"
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func ensureRuntimeStarted() {
|
||||
cfg, err := config.Load("")
|
||||
if err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "runtime autostart skipped config="+err.Error())
|
||||
return
|
||||
}
|
||||
addr := net.JoinHostPort(cfg.OSC.Host, strconv.Itoa(cfg.OSC.Port))
|
||||
probe, err := net.ListenPacket("udp", addr)
|
||||
if err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "runtime already listening addr="+addr+" err="+err.Error())
|
||||
return
|
||||
}
|
||||
_ = probe.Close()
|
||||
|
||||
exe, err := os.Executable()
|
||||
if err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "runtime autostart skipped exe="+err.Error())
|
||||
return
|
||||
}
|
||||
runtimeExe := filepath.Join(filepath.Dir(exe), "vrc_osc.exe")
|
||||
if _, err := os.Stat(runtimeExe); err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "runtime autostart missing exe="+runtimeExe+" err="+err.Error())
|
||||
return
|
||||
}
|
||||
if err := exec.Command(runtimeExe, "--no-gui").Start(); err != nil {
|
||||
_ = app.AppendRuntimeLog("GUI", "runtime autostart failed exe="+runtimeExe+" err="+err.Error())
|
||||
return
|
||||
}
|
||||
_ = app.AppendRuntimeLog("GUI", "runtime autostart launched exe="+runtimeExe)
|
||||
}
|
||||
|
||||
5
cmd/vrc_osc_gui/main_nonwindows.go
Normal file
5
cmd/vrc_osc_gui/main_nonwindows.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build !windows
|
||||
|
||||
package main
|
||||
|
||||
func main() {}
|
||||
BIN
cmd/vrc_osc_gui/rsrc_windows_amd64.syso
Normal file
BIN
cmd/vrc_osc_gui/rsrc_windows_amd64.syso
Normal file
Binary file not shown.
@@ -13,9 +13,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
wmUser = 0x0400
|
||||
wmTray = wmUser + 1
|
||||
wmCommand = 0x0111
|
||||
wmUser = 0x0400
|
||||
wmTray = wmUser + 1
|
||||
wmCommand = 0x0111
|
||||
wmDestroy = 0x0002
|
||||
wmClose = 0x0010
|
||||
wmRButtonUp = 0x0205
|
||||
@@ -37,7 +37,7 @@ const (
|
||||
idOpen = 1001
|
||||
idExit = 1002
|
||||
maxTip = 128
|
||||
maxClassName = 64
|
||||
maxClassName = 64
|
||||
)
|
||||
|
||||
type trayApp struct {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user