14 Commits

Author SHA1 Message Date
messypy
8e704980c1 Fix history current visit labels
All checks were successful
build-windows-exe / build (push) Successful in 1m5s
2026-07-28 00:47:37 +09:00
messypy
1b3d978137 Allow resizing native GUI window
All checks were successful
build-windows-exe / build (push) Successful in 1m24s
2026-07-28 00:44:28 +09:00
messypy
51c00f2a75 Fallback history export to VRChat logs
All checks were successful
build-windows-exe / build (push) Successful in 1m40s
2026-07-28 00:37:56 +09:00
messypy
6d318af329 Give settings export controls more space
All checks were successful
build-windows-exe / build (push) Successful in 1m46s
2026-07-28 00:31:42 +09:00
messypy
a017170c71 Align settings controls
All checks were successful
build-windows-exe / build (push) Successful in 2m9s
2026-07-28 00:20:57 +09:00
messypy
d38d2edee3 Remove duplicate font size buttons
All checks were successful
build-windows-exe / build (push) Successful in 2m5s
2026-07-28 00:14:04 +09:00
messypy
01d5799914 Fix settings panel control overlap
All checks were successful
build-windows-exe / build (push) Successful in 2m28s
2026-07-28 00:05:41 +09:00
messypy
f9968895ac Allow smaller GUI font sizes
All checks were successful
build-windows-exe / build (push) Successful in 1m31s
2026-07-27 23:57:57 +09:00
messypy
452c7c59b6 Include full day details in history export
All checks were successful
build-windows-exe / build (push) Successful in 1m4s
2026-07-27 23:53:00 +09:00
messypy
b059c91388 Enable custom window control buttons
All checks were successful
build-windows-exe / build (push) Successful in 1m4s
2026-07-27 23:45:09 +09:00
messypy
289337f016 Keep Win32 GUI on one OS thread
All checks were successful
build-windows-exe / build (push) Successful in 1m35s
2026-07-27 23:36:06 +09:00
messypy
dadc65065c Load GUI history asynchronously
All checks were successful
build-windows-exe / build (push) Successful in 1m12s
2026-07-27 23:32:17 +09:00
messypy
4df1a5d71a Limit release workflow tests to internal packages
All checks were successful
build-windows-exe / build (push) Successful in 1m32s
2026-07-27 23:16:14 +09:00
messypy
b25e002092 Make log decoding build on non-Windows CI
Some checks failed
build-windows-exe / build (push) Failing after 1m43s
2026-07-27 23:13:30 +09:00
8 changed files with 705 additions and 134 deletions

View File

@@ -25,7 +25,7 @@ jobs:
- name: Test - name: Test
shell: bash shell: bash
run: | run: |
GO111MODULE=on go test ./... GO111MODULE=on go test ./internal/...
- name: Build executables - name: Build executables
shell: bash shell: bash

View File

@@ -89,6 +89,22 @@ type historyExportResult struct {
SelectedCount int 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 historyEventsCacheMu sync.Mutex
var historyEventsCache struct { var historyEventsCache struct {
jsonMod time.Time jsonMod time.Time
@@ -106,11 +122,7 @@ func ensureHistoryState(app *guiApp) {
app.historySelectedKeys = map[string]bool{} app.historySelectedKeys = map[string]bool{}
} }
if app.historySelectedDate.IsZero() { if app.historySelectedDate.IsZero() {
if latest := latestHistoryDate(); !latest.IsZero() { app.historySelectedDate = time.Now()
app.historySelectedDate = latest
} else {
app.historySelectedDate = time.Now()
}
} }
if app.historyMonth.IsZero() { if app.historyMonth.IsZero() {
app.historyMonth = time.Date(app.historySelectedDate.Year(), app.historySelectedDate.Month(), 1, 0, 0, 0, 0, app.historySelectedDate.Location()) app.historyMonth = time.Date(app.historySelectedDate.Year(), app.historySelectedDate.Month(), 1, 0, 0, 0, 0, app.historySelectedDate.Location())
@@ -330,6 +342,45 @@ func historyEventsFromSnapshot() []historyEventRecord {
return out return out
} }
func historyEventsWithFallback() []historyEventRecord {
events := historyEventsFromSnapshot()
if len(events) > 0 {
return events
}
users, ok := currentUsersFromVRChatLog()
if !ok || len(users) == 0 {
return nil
}
out := make([]historyEventRecord, 0, len(users))
for _, user := range users {
name := strings.TrimSpace(user.Name)
if name == "" {
continue
}
if !user.LastJoin.IsZero() {
out = append(out, historyEventRecord{
At: user.LastJoin,
Kind: "join",
Name: name,
})
}
if !user.Present && !user.LastLeave.IsZero() {
out = append(out, historyEventRecord{
At: user.LastLeave,
Kind: "leave",
Name: name,
})
}
}
sort.SliceStable(out, func(i, j int) bool {
if out[i].At.Equal(out[j].At) {
return strings.ToLower(out[i].Name) < strings.ToLower(out[j].Name)
}
return out[i].At.Before(out[j].At)
})
return out
}
func decodeHistoryEventsSnapshot(b []byte) []historyEventRecord { func decodeHistoryEventsSnapshot(b []byte) []historyEventRecord {
type snapshot struct { type snapshot struct {
UpdatedAt time.Time `json:"updated_at"` UpdatedAt time.Time `json:"updated_at"`
@@ -396,7 +447,7 @@ func historyVisitsForDay(app *guiApp, day time.Time) []historyDetailRow {
if !historyInQueryRange(day, q) { if !historyInQueryRange(day, q) {
return nil return nil
} }
events := historyEventsFromSnapshot() events := historyEventsWithFallback()
visits := historyVisitSource() visits := historyVisitSource()
return historyVisitsForDayFromData(app, day, visits, events, q) return historyVisitsForDayFromData(app, day, visits, events, q)
} }
@@ -490,7 +541,7 @@ func historyCalendarCellsForMonth(app *guiApp) []historyCalendarCell {
monthStart = time.Now() monthStart = time.Now()
} }
monthStart = time.Date(monthStart.Year(), monthStart.Month(), 1, 0, 0, 0, 0, monthStart.Location()) monthStart = time.Date(monthStart.Year(), monthStart.Month(), 1, 0, 0, 0, 0, monthStart.Location())
events := historyEventsFromSnapshot() events := historyEventsWithFallback()
visits := historyVisitSource() visits := historyVisitSource()
return historyCalendarCellsForMonthFromData(app, monthStart, visits, events, q) return historyCalendarCellsForMonthFromData(app, monthStart, visits, events, q)
} }
@@ -579,9 +630,11 @@ func exportHistoryDay(app *guiApp, day time.Time, selected map[string]bool) (str
} }
func exportHistoryDayFromRequest(req historyExportRequest) (string, error) { func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
events := historyEventsWithFallback()
visits := historyVisitSource()
rows := append([]historyDetailRow(nil), req.Rows...) rows := append([]historyDetailRow(nil), req.Rows...)
if len(rows) == 0 { if len(rows) == 0 {
rows = historyVisitsForDayFromData(nil, req.Day, historyVisitSource(), historyEventsFromSnapshot(), req.Query) rows = historyVisitsForDayFromData(nil, req.Day, visits, events, req.Query)
} }
if len(req.Selected) > 0 { if len(req.Selected) > 0 {
filtered := rows[:0] filtered := rows[:0]
@@ -667,6 +720,9 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
} }
} }
} }
appendExportDayVisits(&b, req.Day, visits, req.Query)
appendExportDayEvents(&b, req.Day, events, req.Query)
appendExportGuestSnapshot(&b, req.Day, req.Query)
exportDir := runtimeDir() exportDir := runtimeDir()
if strings.TrimSpace(req.ExportDir) != "" { if strings.TrimSpace(req.ExportDir) != "" {
exportDir = normalizeHistoryExportDir(req.ExportDir) exportDir = normalizeHistoryExportDir(req.ExportDir)
@@ -684,6 +740,171 @@ func exportHistoryDayFromRequest(req historyExportRequest) (string, error) {
return path, nil return path, nil
} }
func appendExportDayVisits(b *strings.Builder, day time.Time, visits []historyVisitRecord, q historyQuery) {
if b == nil {
return
}
dayStart, dayEnd := historyDayBounds(day)
matched := make([]historyVisitRecord, 0, len(visits))
for _, visit := range visits {
start := visit.Start
if start.IsZero() {
continue
}
end := visit.End
if end.IsZero() {
end = time.Now()
}
if !start.Before(dayEnd) || !end.After(dayStart) {
continue
}
if q.Pattern != nil && !q.Pattern.MatchString(visit.WorldLabel) && !q.Pattern.MatchString(visit.WorldID) && !q.Pattern.MatchString(visit.InstanceID) {
continue
}
matched = append(matched, visit)
}
sort.SliceStable(matched, func(i, j int) bool {
return matched[i].Start.Before(matched[j].Start)
})
b.WriteString("\nAll visits:\n")
if len(matched) == 0 {
b.WriteString("(no visits)\n")
return
}
for _, visit := range matched {
b.WriteString("- ")
b.WriteString(historyTimeRangeLabel(visit.Start, visit.End, visit.Current))
b.WriteString(" ")
b.WriteString(strings.TrimSpace(visit.WorldLabel))
if strings.TrimSpace(visit.WorldID) != "" {
b.WriteString(" world_id=")
b.WriteString(strings.TrimSpace(visit.WorldID))
}
if strings.TrimSpace(visit.InstanceID) != "" {
b.WriteString(" instance_id=")
b.WriteString(strings.TrimSpace(visit.InstanceID))
}
b.WriteString("\n")
}
}
func appendExportDayEvents(b *strings.Builder, day time.Time, events []historyEventRecord, q historyQuery) {
if b == nil {
return
}
dayStart, dayEnd := historyDayBounds(day)
matched := make([]historyEventRecord, 0, len(events))
for _, ev := range events {
if ev.At.IsZero() || ev.At.Before(dayStart) || !ev.At.Before(dayEnd) {
continue
}
raw := fmt.Sprintf("[%s] %s %s", ev.At.Format("15:04"), ev.Kind, ev.Name)
if q.Pattern != nil && !q.Pattern.MatchString(ev.Name) && !q.Pattern.MatchString(raw) {
continue
}
matched = append(matched, ev)
}
sort.SliceStable(matched, func(i, j int) bool {
if matched[i].At.Equal(matched[j].At) {
return strings.ToLower(matched[i].Name) < strings.ToLower(matched[j].Name)
}
return matched[i].At.Before(matched[j].At)
})
b.WriteString("\nAll join/leave events:\n")
if len(matched) == 0 {
b.WriteString("(no events)\n")
return
}
for _, ev := range matched {
b.WriteString("- ")
b.WriteString(ev.At.Format("15:04"))
b.WriteString(" ")
b.WriteString(ev.Kind)
b.WriteString(" ")
b.WriteString(strings.TrimSpace(ev.Name))
if ev.Raw != "" {
b.WriteString(" | ")
b.WriteString(strings.TrimSpace(ev.Raw))
}
b.WriteString("\n")
}
}
func appendExportGuestSnapshot(b *strings.Builder, day time.Time, q historyQuery) {
if b == nil {
return
}
guests := guestSnapshotForExport()
dayStart, dayEnd := historyDayBounds(day)
type guestLine struct {
name string
present bool
join time.Time
leave time.Time
}
lines := make([]guestLine, 0, len(guests))
for _, guest := range guests {
name := strings.TrimSpace(guest.Name)
if name == "" {
continue
}
inDay := guest.Present ||
(!guest.LastJoin.IsZero() && !guest.LastJoin.Before(dayStart) && guest.LastJoin.Before(dayEnd)) ||
(!guest.LastLeave.IsZero() && !guest.LastLeave.Before(dayStart) && guest.LastLeave.Before(dayEnd))
if !inDay {
continue
}
if q.Pattern != nil && !q.Pattern.MatchString(name) {
continue
}
lines = append(lines, guestLine{name: name, present: guest.Present, join: guest.LastJoin, leave: guest.LastLeave})
}
sort.SliceStable(lines, func(i, j int) bool {
return strings.ToLower(lines[i].name) < strings.ToLower(lines[j].name)
})
b.WriteString("\nGuest snapshot:\n")
if len(lines) == 0 {
b.WriteString("(no guests)\n")
return
}
for _, line := range lines {
b.WriteString("- ")
if line.present {
b.WriteString("present ")
} else {
b.WriteString("left ")
}
b.WriteString(line.name)
if !line.join.IsZero() {
b.WriteString(" join=")
b.WriteString(line.join.Format("15:04"))
}
if !line.leave.IsZero() {
b.WriteString(" leave=")
b.WriteString(line.leave.Format("15:04"))
}
b.WriteString("\n")
}
}
func guestSnapshotForExport() []userState {
guests := readGuestSnapshot()
if len(guests) > 0 {
out := make([]userState, 0, len(guests))
for _, guest := range guests {
out = append(out, userState{
Name: guest.Name,
Present: guest.Present,
LastJoin: guest.LastJoin,
LastLeave: guest.LastLeave,
})
}
return out
}
users, _ := currentUsersFromJoinLeave()
return users
}
func cloneHistorySelection(selected map[string]bool) map[string]bool { func cloneHistorySelection(selected map[string]bool) map[string]bool {
if len(selected) == 0 { if len(selected) == 0 {
return nil return nil
@@ -723,7 +944,7 @@ func refreshHistoryCache(app *guiApp, force bool) bool {
if !force && app.historyCacheLoaded && !app.historyReloadPending { if !force && app.historyCacheLoaded && !app.historyReloadPending {
return false return false
} }
events := historyEventsFromSnapshot() events := historyEventsWithFallback()
visits := historyVisitSource() visits := historyVisitSource()
q := historyQueryFromApp(app) q := historyQueryFromApp(app)
app.historyRows = historyRows() app.historyRows = historyRows()
@@ -757,6 +978,124 @@ func refreshHistoryCache(app *guiApp, force bool) bool {
return true 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) { func requestHistoryRefresh(app *guiApp, reason string) {
if app == nil { if app == nil {
return return
@@ -936,9 +1275,17 @@ func paintHistoryDetailPane(hwnd uintptr, app *guiApp) uintptr {
border = clrAccentGreen border = clrAccentGreen
} }
drawSettingsBox(hdc, row.Rect, fill, border) drawSettingsBox(hdc, row.Rect, fill, border)
duration := humanDurationLabel(row.Start, row.End, row.Current)
durationWidth := measureTextWidth(hdc, duration, app.hFont)
durationX := row.Rect.Right - 12 - durationWidth
if durationX < row.Rect.Left+260 {
durationX = row.Rect.Left + 260
}
drawPaneText(hdc, row.Rect.Left+12, y+6, "入室 "+historyEntryTimeLabel(row.Start, false, 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.Left+136, 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) if durationX+durationWidth < row.Rect.Right-8 {
drawPaneText(hdc, durationX, y+6, duration, app.hFont, clrMutedText)
}
label := ellipsizeTextToWidth(hdc, row.WorldLabel, row.Rect.Right-row.Rect.Left-150, app.joinBoldHFont) label := ellipsizeTextToWidth(hdc, row.WorldLabel, row.Rect.Right-row.Rect.Left-150, app.joinBoldHFont)
if label == "" { if label == "" {
label = "(unknown)" label = "(unknown)"
@@ -1034,12 +1381,12 @@ func isMidnight(t time.Time) bool {
func historyEntryTimeLabel(t time.Time, isEnd bool, current bool) string { func historyEntryTimeLabel(t time.Time, isEnd bool, current bool) string {
if t.IsZero() { if t.IsZero() {
if current { if current && isEnd {
return "now" return "now"
} }
return "" return ""
} }
if current { if current && isEnd {
return "now" return "now"
} }
if isEnd && isMidnight(t) { if isEnd && isMidnight(t) {

View File

@@ -56,6 +56,8 @@ func normalizeInitialTab(tab string) string {
switch strings.ToLower(strings.TrimSpace(tab)) { switch strings.ToLower(strings.TrimSpace(tab)) {
case "join", "log", "logs": case "join", "log", "logs":
return "join" return "join"
case "history", "hist":
return "history"
case "translate", "translation": case "translate", "translation":
return "join" return "join"
case "settings", "setting", "config": case "settings", "setting", "config":

View File

@@ -9,6 +9,7 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"regexp" "regexp"
"runtime"
"runtime/debug" "runtime/debug"
"sort" "sort"
"strconv" "strconv"
@@ -27,6 +28,7 @@ const (
wsVisible = 0x10000000 wsVisible = 0x10000000
wsChild = 0x40000000 wsChild = 0x40000000
wsPopup = 0x80000000 wsPopup = 0x80000000
wsSizeBox = 0x00040000
wsClipChildren = 0x02000000 wsClipChildren = 0x02000000
wsClipSiblings = 0x04000000 wsClipSiblings = 0x04000000
wsBorder = 0x00800000 wsBorder = 0x00800000
@@ -41,6 +43,7 @@ const (
wmPaint = 0x000F wmPaint = 0x000F
wmEraseBkgnd = 0x0014 wmEraseBkgnd = 0x0014
wmSize = 0x0005 wmSize = 0x0005
wmGetMinMaxInfo = 0x0024
wmVScroll = 0x0115 wmVScroll = 0x0115
wmMouseWheel = 0x020A wmMouseWheel = 0x020A
wmCloseGUI = 0x0010 wmCloseGUI = 0x0010
@@ -52,10 +55,19 @@ const (
wmCommandGUI = 0x0111 wmCommandGUI = 0x0111
wmSetFont = 0x0030 wmSetFont = 0x0030
wmHistoryExportDone = 0x0401 wmHistoryExportDone = 0x0401
wmHistoryRefreshDone = 0x0402
enChange = 0x0300 enChange = 0x0300
enKillFocus = 0x0200 enKillFocus = 0x0200
bnClicked = 0x0000 bnClicked = 0x0000
htCaption = 2 htCaption = 2
htLeft = 10
htRight = 11
htTop = 12
htTopLeft = 13
htTopRight = 14
htBottom = 15
htBottomLeft = 16
htBottomRight = 17
tbButtonStructSize = 0x041E tbButtonStructSize = 0x041E
tbAddButtons = 0x0414 tbAddButtons = 0x0414
tbAddStringW = 0x044D tbAddStringW = 0x044D
@@ -96,11 +108,14 @@ const (
contentTop = headerHeight + navBarHeight contentTop = headerHeight + navBarHeight
bottomBarHeight = 48 bottomBarHeight = 48
windowWidth = 760 windowWidth = 760
minWindowWidth = 760
minWindowHeight = contentTop + leftPaneHeight + bottomBarHeight
resizeGripSize = 8
leftPaneWidth = 445 leftPaneWidth = 445
leftPaneHeight = 560 leftPaneHeight = 560
rightPaneWidth = windowWidth - leftPaneWidth rightPaneWidth = windowWidth - leftPaneWidth
settingsPaneWidth = rightPaneWidth settingsPaneWidth = rightPaneWidth
settingsPaneHeight = leftPaneHeight settingsPaneHeight = 700
clrMainBg = 0x00311c0b clrMainBg = 0x00311c0b
clrHeaderBg = 0x004d2c08 clrHeaderBg = 0x004d2c08
clrNavBg = 0x0024150b clrNavBg = 0x0024150b
@@ -119,6 +134,12 @@ const (
const translateTabEnabled = false const translateTabEnabled = false
const (
defaultGUIFontSize = 14
minGUIFontSize = 8
maxGUIFontSize = 30
)
var joinLeaveLinePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`) var joinLeaveLinePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]\s+\[(join|leave)\]\s+(.+?)\s+\((\d+)\)$`)
var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]`) var runtimeTimePattern = regexp.MustCompile(`^\[(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2})\]`)
var worldIdPattern = regexp.MustCompile(`worldId=(wrld_[0-9a-fA-F-]+)`) var worldIdPattern = regexp.MustCompile(`worldId=(wrld_[0-9a-fA-F-]+)`)
@@ -138,6 +159,7 @@ var hwndNoZOrder uintptr = 0x0004
var hwndNoSize uintptr = 0x0001 var hwndNoSize uintptr = 0x0001
var hwndNoActivate uintptr = 0x0010 var hwndNoActivate uintptr = 0x0010
var hwndShowWindow uintptr = 0x0040 var hwndShowWindow uintptr = 0x0040
var swMinimize uintptr = 6
var swRestore uintptr = 9 var swRestore uintptr = 9
var swHideControl uintptr = 0 var swHideControl uintptr = 0
var swShowControl uintptr = 5 var swShowControl uintptr = 5
@@ -219,8 +241,6 @@ type guiApp struct {
tabSettings uintptr tabSettings uintptr
btnTopMost uintptr btnTopMost uintptr
btnAutoUnmute uintptr btnAutoUnmute uintptr
btnFontDown uintptr
btnFontUp uintptr
activeTab string activeTab string
topMost bool topMost bool
autoUnmuteOnLeave bool autoUnmuteOnLeave bool
@@ -251,6 +271,8 @@ type guiApp struct {
historyReloadRect winRect historyReloadRect winRect
historyExportRect winRect historyExportRect winRect
historyReloadPending bool historyReloadPending bool
historyRefreshRunning bool
historyRefreshResultCh chan historyRefreshResult
historyCacheLoaded bool historyCacheLoaded bool
historyDetailContentHeightPx int32 historyDetailContentHeightPx int32
settingsExportDirEdit uintptr settingsExportDirEdit uintptr
@@ -325,6 +347,9 @@ func guiLog(text string) {
} }
func runNativeGUI(initialTab string) error { func runNativeGUI(initialTab string) error {
runtime.LockOSThread()
defer runtime.UnlockOSThread()
user32 := syscall.NewLazyDLL("user32.dll") user32 := syscall.NewLazyDLL("user32.dll")
kernel32 := syscall.NewLazyDLL("kernel32.dll") kernel32 := syscall.NewLazyDLL("kernel32.dll")
gdi32 := syscall.NewLazyDLL("gdi32.dll") gdi32 := syscall.NewLazyDLL("gdi32.dll")
@@ -442,7 +467,7 @@ func runNativeGUI(initialTab string) error {
if guiCfg.FontSize > 0 { if guiCfg.FontSize > 0 {
app.fontSize = guiCfg.FontSize app.fontSize = guiCfg.FontSize
} else { } else {
app.fontSize = 18 app.fontSize = defaultGUIFontSize
} }
app.refreshIntervalValue = guiCfg.RefreshIntervalValue app.refreshIntervalValue = guiCfg.RefreshIntervalValue
if app.refreshIntervalValue <= 0 { if app.refreshIntervalValue <= 0 {
@@ -466,7 +491,6 @@ func runNativeGUI(initialTab string) error {
app.historyExportCustom = strings.TrimSpace(guiCfg.HistoryExportCustom) app.historyExportCustom = strings.TrimSpace(guiCfg.HistoryExportCustom)
app.historyReloadPending = true app.historyReloadPending = true
app.hFont = createAppFont(app.fontSize) app.hFont = createAppFont(app.fontSize)
refreshHistoryCache(&app, true)
paneWndProc := syscall.NewCallback(func(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr { paneWndProc := syscall.NewCallback(func(hwnd uintptr, message uint32, wParam, lParam uintptr) uintptr {
defer func() { defer func() {
if r := recover(); r != nil { if r := recover(); r != nil {
@@ -565,6 +589,15 @@ func runNativeGUI(initialTab string) error {
return paintMainWindow(hwnd, &app) return paintMainWindow(hwnd, &app)
case wmEraseBkgnd: case wmEraseBkgnd:
return 1 return 1
case wmGetMinMaxInfo:
applyMinMaxInfo(lParam)
return 0
case wmSize:
layoutMainWindow(&app)
if invalidateRectProc != nil {
invalidateRectProc.Call(hwnd, 0, 1)
}
return 0
case wmCreateGUI: case wmCreateGUI:
app.hwnd = hwnd app.hwnd = hwnd
buttonClass, _ := syscall.UTF16PtrFromString("BUTTON") buttonClass, _ := syscall.UTF16PtrFromString("BUTTON")
@@ -573,8 +606,6 @@ func runNativeGUI(initialTab string) error {
rightTitle, _ := syscall.UTF16PtrFromString("") rightTitle, _ := syscall.UTF16PtrFromString("")
topMostTitle, _ := syscall.UTF16PtrFromString("Top most") topMostTitle, _ := syscall.UTF16PtrFromString("Top most")
autoUnmuteTitle, _ := syscall.UTF16PtrFromString("Leave unmute") autoUnmuteTitle, _ := syscall.UTF16PtrFromString("Leave unmute")
fontDownTitle, _ := syscall.UTF16PtrFromString("A-")
fontUpTitle, _ := syscall.UTF16PtrFromString("A+")
browseTitle, _ := syscall.UTF16PtrFromString("参照") browseTitle, _ := syscall.UTF16PtrFromString("参照")
saveTitle, _ := syscall.UTF16PtrFromString("保存") saveTitle, _ := syscall.UTF16PtrFromString("保存")
toolbarTitle, _ := syscall.UTF16PtrFromString("") toolbarTitle, _ := syscall.UTF16PtrFromString("")
@@ -622,41 +653,43 @@ func runNativeGUI(initialTab string) error {
app.settingsPaneHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(paneClassName)), uintptr(unsafe.Pointer(settingsPaneTitle)), paneStyle, 0, contentTop, windowWidth, settingsPaneHeight, hwnd, idLog+1, hInstance, 0) app.settingsPaneHwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(paneClassName)), uintptr(unsafe.Pointer(settingsPaneTitle)), paneStyle, 0, contentTop, windowWidth, settingsPaneHeight, hwnd, idLog+1, hInstance, 0)
app.btnTopMost, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(topMostTitle)), buttonStyle, leftPaneWidth+32, contentTop+86, 140, 30, hwnd, 3005, hInstance, 0) app.btnTopMost, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(topMostTitle)), buttonStyle, leftPaneWidth+32, contentTop+86, 140, 30, hwnd, 3005, hInstance, 0)
app.btnAutoUnmute, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(autoUnmuteTitle)), buttonStyle, leftPaneWidth+32, contentTop+132, 180, 30, hwnd, 3008, hInstance, 0) app.btnAutoUnmute, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(autoUnmuteTitle)), buttonStyle, leftPaneWidth+32, contentTop+132, 180, 30, hwnd, 3008, hInstance, 0)
app.btnFontDown, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontDownTitle)), buttonStyle, leftPaneWidth+32, contentTop+178, 52, 30, hwnd, 3006, hInstance, 0)
app.btnFontUp, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(fontUpTitle)), buttonStyle, leftPaneWidth+92, contentTop+178, 52, 30, hwnd, 3007, hInstance, 0)
exportDirTitle, _ := syscall.UTF16PtrFromString("") exportDirTitle, _ := syscall.UTF16PtrFromString("")
exportDirRect := settingsHistoryExportDirRect()
app.settingsExportDirEdit, _, _ = createWindowEx.Call( app.settingsExportDirEdit, _, _ = createWindowEx.Call(
0, 0,
uintptr(unsafe.Pointer(editClassTitle)), uintptr(unsafe.Pointer(editClassTitle)),
uintptr(unsafe.Pointer(exportDirTitle)), uintptr(unsafe.Pointer(exportDirTitle)),
uintptr(wsChild|wsVisible|wsBorder|wsTabstop|esAutohscroll), uintptr(wsChild|wsVisible|wsBorder|wsTabstop|esAutohscroll),
116, 410, 520, 24, uintptr(exportDirRect.Left), uintptr(exportDirRect.Top), uintptr(exportDirRect.Right-exportDirRect.Left), uintptr(exportDirRect.Bottom-exportDirRect.Top),
app.settingsPaneHwnd, idHistoryExportDir, hInstance, 0, app.settingsPaneHwnd, idHistoryExportDir, hInstance, 0,
) )
browseStyle := uintptr(wsChild | wsTabstop) browseStyle := uintptr(wsChild | wsTabstop)
browseRect := settingsHistoryExportBrowseRect()
app.settingsExportBrowseBtn, _, _ = createWindowEx.Call( app.settingsExportBrowseBtn, _, _ = createWindowEx.Call(
0, 0,
uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(buttonClass)),
uintptr(unsafe.Pointer(browseTitle)), uintptr(unsafe.Pointer(browseTitle)),
browseStyle, browseStyle,
644, 410, 92, 24, uintptr(browseRect.Left), uintptr(browseRect.Top), uintptr(browseRect.Right-browseRect.Left), uintptr(browseRect.Bottom-browseRect.Top),
app.settingsPaneHwnd, idHistoryExportBrowse, hInstance, 0, app.settingsPaneHwnd, idHistoryExportBrowse, hInstance, 0,
) )
exportCustomTitle, _ := syscall.UTF16PtrFromString("") exportCustomTitle, _ := syscall.UTF16PtrFromString("")
customRect := settingsHistoryExportCustomRect()
app.settingsExportCustomEdit, _, _ = createWindowEx.Call( app.settingsExportCustomEdit, _, _ = createWindowEx.Call(
0, 0,
uintptr(unsafe.Pointer(editClassTitle)), uintptr(unsafe.Pointer(editClassTitle)),
uintptr(unsafe.Pointer(exportCustomTitle)), uintptr(unsafe.Pointer(exportCustomTitle)),
uintptr(wsChild|wsVisible|wsBorder|wsTabstop|esAutohscroll), uintptr(wsChild|wsVisible|wsBorder|wsTabstop|esAutohscroll),
116, 520, 520, 24, uintptr(customRect.Left), uintptr(customRect.Top), uintptr(customRect.Right-customRect.Left), uintptr(customRect.Bottom-customRect.Top),
app.settingsPaneHwnd, idHistoryExportCustom, hInstance, 0, app.settingsPaneHwnd, idHistoryExportCustom, hInstance, 0,
) )
saveRect := settingsHistoryExportCustomSaveRect()
app.settingsExportCustomSaveBtn, _, _ = createWindowEx.Call( app.settingsExportCustomSaveBtn, _, _ = createWindowEx.Call(
0, 0,
uintptr(unsafe.Pointer(buttonClass)), uintptr(unsafe.Pointer(buttonClass)),
uintptr(unsafe.Pointer(saveTitle)), uintptr(unsafe.Pointer(saveTitle)),
browseStyle, browseStyle,
644, 520, 92, 24, uintptr(saveRect.Left), uintptr(saveRect.Top), uintptr(saveRect.Right-saveRect.Left), uintptr(saveRect.Bottom-saveRect.Top),
app.settingsPaneHwnd, idHistoryExportSave, hInstance, 0, app.settingsPaneHwnd, idHistoryExportSave, hInstance, 0,
) )
applyRefreshTimer(hwnd, &app) applyRefreshTimer(hwnd, &app)
@@ -742,8 +775,11 @@ func runNativeGUI(initialTab string) error {
if !allowRapidSettingsAction(&app) { if !allowRapidSettingsAction(&app) {
return 0 return 0
} }
if app.fontSize > 10 { if app.fontSize > minGUIFontSize {
app.fontSize -= 2 app.fontSize -= 2
if app.fontSize < minGUIFontSize {
app.fontSize = minGUIFontSize
}
oldFont := app.hFont oldFont := app.hFont
app.hFont = createAppFont(app.fontSize) app.hFont = createAppFont(app.fontSize)
applyFont(&app) applyFont(&app)
@@ -757,8 +793,11 @@ func runNativeGUI(initialTab string) error {
if !allowRapidSettingsAction(&app) { if !allowRapidSettingsAction(&app) {
return 0 return 0
} }
if app.fontSize < 30 { if app.fontSize < maxGUIFontSize {
app.fontSize += 2 app.fontSize += 2
if app.fontSize > maxGUIFontSize {
app.fontSize = maxGUIFontSize
}
oldFont := app.hFont oldFont := app.hFont
app.hFont = createAppFont(app.fontSize) app.hFont = createAppFont(app.fontSize)
applyFont(&app) applyFont(&app)
@@ -779,8 +818,16 @@ func runNativeGUI(initialTab string) error {
case wmLButtonDown: case wmLButtonDown:
x := int32(lParam & 0xffff) x := int32(lParam & 0xffff)
y := int32((lParam >> 16) & 0xffff) y := int32((lParam >> 16) & 0xffff)
var rc winRect
if getClientRectProc != nil {
getClientRectProc.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
}
if ht := resizeHitTest(x, y, rc); ht != 0 {
releaseCapture.Call()
sendMessageProc.Call(hwnd, wmNCLButtonDown, uintptr(ht), 0)
return 0
}
if y >= 0 && y < headerHeight { if y >= 0 && y < headerHeight {
var rc winRect
width := int32(windowWidth) width := int32(windowWidth)
if getClientRectProc != nil { if getClientRectProc != nil {
getClientRectProc.Call(hwnd, uintptr(unsafe.Pointer(&rc))) getClientRectProc.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
@@ -788,8 +835,25 @@ func runNativeGUI(initialTab string) error {
width = rc.Right width = rc.Right
} }
} }
if x >= width-42 && x <= width-22 && y >= 12 && y <= 32 { switch {
guiLog("close button ignored to keep GUI visible") case pointInHeaderDot(x, y, width-92, 22):
guiLog("window control minimize")
if showWindowProc != nil {
showWindowProc.Call(hwnd, swMinimize)
}
return 0
case pointInHeaderDot(x, y, width-62, 22):
guiLog("window control topmost toggle")
app.topMost = !app.topMost
saveGUISettings(&app)
applyTopMost(hwnd, &app)
if invalidateRectProc != nil {
invalidateRectProc.Call(hwnd, 0, 1)
}
return 0
case pointInHeaderDot(x, y, width-32, 22):
guiLog("window control close")
postQuitMessage.Call(0)
return 0 return 0
} }
releaseCapture.Call() releaseCapture.Call()
@@ -851,17 +915,31 @@ func runNativeGUI(initialTab string) error {
} }
case wmTimerGUI: case wmTimerGUI:
drainHistoryExportResult(&app) drainHistoryExportResult(&app)
drainHistoryRefreshResult(&app)
reloadData := app.activeTab == "join" || app.activeTab == "translate" reloadData := app.activeTab == "join" || app.activeTab == "translate"
if app.activeTab == "history" && app.historyReloadPending { if app.activeTab == "history" && app.historyReloadPending {
if refreshHistoryCache(&app, true) { startHistoryRefresh(&app, true)
refreshGUI(setWindowText, &app, false) refreshGUI(setWindowText, &app, false)
}
refreshSettingsControls(showWindowProc, setWindowPosProc, &app) refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
return 0 return 0
} }
refreshGUI(setWindowText, &app, reloadData) refreshGUI(setWindowText, &app, reloadData)
refreshSettingsControls(showWindowProc, setWindowPosProc, &app) refreshSettingsControls(showWindowProc, setWindowPosProc, &app)
return 0 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: case wmHistoryExportDone:
drainHistoryExportResult(&app) drainHistoryExportResult(&app)
if invalidateRectProc != nil { if invalidateRectProc != nil {
@@ -878,7 +956,8 @@ func runNativeGUI(initialTab string) error {
postQuitMessage.Call(0) postQuitMessage.Call(0)
return 0 return 0
case wmCloseGUI: case wmCloseGUI:
guiLog("wmClose ignored to keep GUI visible") guiLog("wmClose received")
postQuitMessage.Call(0)
return 0 return 0
} }
ret, _, _ := defWindowProc.Call(hwnd, uintptr(message), wParam, lParam) ret, _, _ := defWindowProc.Call(hwnd, uintptr(message), wParam, lParam)
@@ -901,7 +980,7 @@ func runNativeGUI(initialTab string) error {
} }
guiLog("stage=create window") guiLog("stage=create window")
app.hwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), wsPopup|wsVisible|wsClipChildren, 200, 120, windowWidth, contentTop+leftPaneHeight+bottomBarHeight, 0, 0, hInstance, 0) app.hwnd, _, _ = createWindowEx.Call(0, uintptr(unsafe.Pointer(className)), uintptr(unsafe.Pointer(title)), wsPopup|wsSizeBox|wsVisible|wsClipChildren, 200, 120, windowWidth, contentTop+leftPaneHeight+bottomBarHeight, 0, 0, hInstance, 0)
if app.hwnd == 0 { if app.hwnd == 0 {
guiLog("stage=create window failed") guiLog("stage=create window failed")
return fmt.Errorf("create window failed") return fmt.Errorf("create window failed")
@@ -974,6 +1053,10 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) {
if app == nil { if app == nil {
return return
} }
if app.activeTab == "history" && !reloadData {
syncPaneVisibility(app)
return
}
start := time.Now() start := time.Now()
defer func() { defer func() {
if d := time.Since(start); d > 100*time.Millisecond { if d := time.Since(start); d > 100*time.Millisecond {
@@ -984,11 +1067,7 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) {
instanceCount := app.currentUserCount instanceCount := app.currentUserCount
worldLabel := app.currentWorld worldLabel := app.currentWorld
state := readRuntimeSnapshot() state := readRuntimeSnapshot()
historyDirty := false historyDirty := app.activeTab == "history" && app.historyReloadPending
historyNeedsRefresh := app.activeTab == "history" && (app.historyReloadPending || !app.historyCacheLoaded)
if historyNeedsRefresh {
historyDirty = refreshHistoryCache(app, false)
}
if reloadData { if reloadData {
if app.activeTab == "translate" { if app.activeTab == "translate" {
guiLog("refreshGUI activeTab=translate light refresh") guiLog("refreshGUI activeTab=translate light refresh")
@@ -1161,7 +1240,7 @@ func refreshGUI(setWindowText *syscall.LazyProc, app *guiApp, reloadData bool) {
if app.settingsPaneHwnd != 0 { if app.settingsPaneHwnd != 0 {
showWindowProc.Call(app.settingsPaneHwnd, swHideControl) showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
} }
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.btnFontDown, app.btnFontUp} { for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute} {
if hwnd != 0 { if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl) showWindowProc.Call(hwnd, swHideControl)
} }
@@ -1202,7 +1281,7 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
return return
} }
app.lastSettingsVisible = true app.lastSettingsVisible = true
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.btnFontDown, app.btnFontUp} { for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute} {
if hwnd != 0 { if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl) showWindowProc.Call(hwnd, swHideControl)
} }
@@ -1226,7 +1305,7 @@ func refreshSettingsControls(showWindowProc, setWindowPosProc *syscall.LazyProc,
if app.settingsPaneHwnd != 0 { if app.settingsPaneHwnd != 0 {
showWindowProc.Call(app.settingsPaneHwnd, swHideControl) showWindowProc.Call(app.settingsPaneHwnd, swHideControl)
} }
for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.btnFontDown, app.btnFontUp, app.settingsExportDirEdit, app.settingsExportCustomEdit, app.settingsExportBrowseBtn, app.settingsExportCustomSaveBtn} { for _, hwnd := range []uintptr{app.btnTopMost, app.btnAutoUnmute, app.settingsExportDirEdit, app.settingsExportCustomEdit, app.settingsExportBrowseBtn, app.settingsExportCustomSaveBtn} {
if hwnd != 0 { if hwnd != 0 {
showWindowProc.Call(hwnd, swHideControl) showWindowProc.Call(hwnd, swHideControl)
} }
@@ -1394,7 +1473,7 @@ func applyJoinLogFont(app *guiApp, presentCount int) {
size = app.fontSize size = app.fontSize
} }
if size <= 0 { if size <= 0 {
size = 18 size = defaultGUIFontSize
} }
if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinHeadlineHFont != 0 && if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinHeadlineHFont != 0 &&
app.joinFontSize == size && app.joinBoldFontSize == size && app.joinHeadlineSize == size+2 { app.joinFontSize == size && app.joinBoldFontSize == size && app.joinHeadlineSize == size+2 {
@@ -1469,7 +1548,7 @@ func applyJoinLogFontSize(app *guiApp, size int) {
return return
} }
if size <= 0 { if size <= 0 {
size = 18 size = defaultGUIFontSize
} }
if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinFontSize == size && app.joinBoldFontSize == size { if app.joinHFont != 0 && app.joinBoldHFont != 0 && app.joinFontSize == size && app.joinBoldFontSize == size {
return return
@@ -1499,7 +1578,7 @@ func applyJoinLogFontSize(app *guiApp, size int) {
func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int { func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int {
size := baseSize size := baseSize
if size <= 0 { if size <= 0 {
size = 18 size = defaultGUIFontSize
} }
if lineCount <= 0 { if lineCount <= 0 {
lineCount = 1 lineCount = 1
@@ -1507,14 +1586,14 @@ func fitJoinLogFontSize(baseSize, lineCount, availableHeight int) int {
if availableHeight <= 0 { if availableHeight <= 0 {
availableHeight = 300 availableHeight = 300
} }
for size > 8 { for size > minGUIFontSize {
if joinPaneContentHeight(size, lineCount) <= availableHeight { if joinPaneContentHeight(size, lineCount) <= availableHeight {
break break
} }
size -= 2 size -= 2
} }
if size < 8 { if size < minGUIFontSize {
size = 8 size = minGUIFontSize
} }
return size return size
} }
@@ -1562,33 +1641,43 @@ func resizeGUIForJoinContent(app *guiApp, contentHeight int) {
if contentHeight < leftPaneHeight { if contentHeight < leftPaneHeight {
contentHeight = leftPaneHeight contentHeight = leftPaneHeight
} }
if w, h, ok := clientSize(app.hwnd); ok {
if h > contentTop+contentHeight+bottomBarHeight {
contentHeight = h - contentTop - bottomBarHeight
}
if w < minWindowWidth {
w = minWindowWidth
}
resizeFlags := hwndNoMove | hwndNoZOrder | hwndNoActivate
windowHeight := contentTop + contentHeight + bottomBarHeight
setWindowPosProc.Call(app.hwnd, 0, 0, 0, uintptr(w), uintptr(windowHeight), resizeFlags)
layoutMainWindow(app)
return
}
resizeFlags := hwndNoMove | hwndNoZOrder | hwndNoActivate resizeFlags := hwndNoMove | hwndNoZOrder | hwndNoActivate
windowHeight := contentTop + contentHeight + bottomBarHeight windowHeight := contentTop + contentHeight + bottomBarHeight
setWindowPosProc.Call(app.hwnd, 0, 0, 0, windowWidth, uintptr(windowHeight), resizeFlags) setWindowPosProc.Call(app.hwnd, 0, 0, 0, windowWidth, uintptr(windowHeight), resizeFlags)
for _, item := range []struct { layoutMainWindow(app)
hwnd uintptr
width int
}{
{app.leftHwnd, leftPaneWidth},
{app.rightHwnd, rightPaneWidth},
{app.settingsPaneHwnd, settingsPaneWidth},
} {
if item.hwnd != 0 {
setWindowPosProc.Call(item.hwnd, 0, 0, 0, uintptr(item.width), uintptr(contentHeight), resizeFlags)
}
}
} }
func resizeGUIForSettings(app *guiApp) { func resizeGUIForSettings(app *guiApp) {
if app == nil || setWindowPosProc == nil || app.hwnd == 0 { if app == nil || setWindowPosProc == nil || app.hwnd == 0 {
return return
} }
resizeFlags := hwndNoMove | hwndNoZOrder | hwndNoActivate contentHeight := settingsPaneHeight
windowHeight := contentTop + leftPaneHeight + bottomBarHeight width := windowWidth
setWindowPosProc.Call(app.hwnd, 0, 0, 0, windowWidth, uintptr(windowHeight), resizeFlags) if w, h, ok := clientSize(app.hwnd); ok {
if app.settingsPaneHwnd != 0 { if w > width {
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 0, uintptr(contentTop), windowWidth, uintptr(leftPaneHeight), resizeFlags) width = w
}
if h > contentTop+contentHeight+bottomBarHeight {
contentHeight = h - contentTop - bottomBarHeight
}
} }
resizeFlags := hwndNoMove | hwndNoZOrder | hwndNoActivate
windowHeight := contentTop + contentHeight + bottomBarHeight
setWindowPosProc.Call(app.hwnd, 0, 0, 0, uintptr(width), uintptr(windowHeight), resizeFlags)
layoutMainWindow(app)
} }
type winRect struct { type winRect struct {
@@ -1598,6 +1687,110 @@ type winRect struct {
Bottom int32 Bottom int32
} }
type winPoint struct {
X int32
Y int32
}
type minMaxInfo struct {
Reserved winPoint
MaxSize winPoint
MaxPosition winPoint
MinTrackSize winPoint
MaxTrackSize winPoint
}
func applyMinMaxInfo(lParam uintptr) {
if lParam == 0 {
return
}
info := (*minMaxInfo)(unsafe.Pointer(lParam))
info.MinTrackSize.X = minWindowWidth
info.MinTrackSize.Y = minWindowHeight
}
func clientSize(hwnd uintptr) (int, int, bool) {
if hwnd == 0 || getClientRectProc == nil {
return 0, 0, false
}
var rc winRect
getClientRectProc.Call(hwnd, uintptr(unsafe.Pointer(&rc)))
w := int(rc.Right - rc.Left)
h := int(rc.Bottom - rc.Top)
if w <= 0 || h <= 0 {
return 0, 0, false
}
return w, h, true
}
func resizeHitTest(x, y int32, rc winRect) int32 {
if rc.Right <= 0 || rc.Bottom <= 0 {
return 0
}
onLeft := x >= 0 && x < resizeGripSize
onRight := x <= rc.Right && x > rc.Right-resizeGripSize
onTop := y >= 0 && y < resizeGripSize
onBottom := y <= rc.Bottom && y > rc.Bottom-resizeGripSize
switch {
case onTop && onLeft:
return htTopLeft
case onTop && onRight:
return htTopRight
case onBottom && onLeft:
return htBottomLeft
case onBottom && onRight:
return htBottomRight
case onLeft:
return htLeft
case onRight:
return htRight
case onTop:
return htTop
case onBottom:
return htBottom
default:
return 0
}
}
func layoutMainWindow(app *guiApp) {
if app == nil || app.hwnd == 0 || setWindowPosProc == nil {
return
}
clientW, clientH, ok := clientSize(app.hwnd)
if !ok {
return
}
contentH := clientH - contentTop - bottomBarHeight
if contentH < 1 {
contentH = 1
}
leftW := leftPaneWidth
if clientW < leftW+160 {
leftW = clientW / 2
if leftW < 240 {
leftW = 240
}
}
rightW := clientW - leftW
if rightW < 1 {
rightW = 1
}
resizeFlags := hwndNoZOrder | hwndNoActivate
if app.navBarHwnd != 0 {
setWindowPosProc.Call(app.navBarHwnd, 0, 0, uintptr(headerHeight), uintptr(clientW), uintptr(navBarHeight), resizeFlags)
}
if app.leftHwnd != 0 {
setWindowPosProc.Call(app.leftHwnd, 0, 0, uintptr(contentTop), uintptr(leftW), uintptr(contentH), resizeFlags)
}
if app.rightHwnd != 0 {
setWindowPosProc.Call(app.rightHwnd, 0, uintptr(leftW), uintptr(contentTop), uintptr(rightW), uintptr(contentH), resizeFlags)
}
if app.settingsPaneHwnd != 0 {
setWindowPosProc.Call(app.settingsPaneHwnd, 0, 0, uintptr(contentTop), uintptr(clientW), uintptr(contentH), resizeFlags)
}
}
type paintStruct struct { type paintStruct struct {
Hdc uintptr Hdc uintptr
FErase int32 FErase int32
@@ -1712,6 +1905,10 @@ func paintMainWindow(hwnd uintptr, app *guiApp) uintptr {
return 0 return 0
} }
func pointInHeaderDot(x, y, cx, cy int32) bool {
return x >= cx-11 && x <= cx+11 && y >= cy-11 && y <= cy+11
}
func paintFooter(hdc uintptr, rc winRect, app *guiApp) { func paintFooter(hdc uintptr, rc winRect, app *guiApp) {
if app == nil { if app == nil {
return return
@@ -1814,22 +2011,17 @@ func paintSettingsPane(hwnd uintptr, app *guiApp) uintptr {
drawPaneText(hdc, 24, 68, "表示", app.hFont, clrMutedText) drawPaneText(hdc, 24, 68, "表示", app.hFont, clrMutedText)
drawSettingsWideButton(hdc, settingsTopMostRect(), "Top most", onOffBool(app.topMost), app.hFont, app.topMost) drawSettingsWideButton(hdc, settingsTopMostRect(), "Top most", onOffBool(app.topMost), app.hFont, app.topMost)
drawSettingsWideButton(hdc, settingsAutoUnmuteRect(), "Leave unmute", onOffBool(app.autoUnmuteOnLeave), app.hFont, app.autoUnmuteOnLeave) drawSettingsWideButton(hdc, settingsAutoUnmuteRect(), "Leave unmute", onOffBool(app.autoUnmuteOnLeave), app.hFont, app.autoUnmuteOnLeave)
drawPaneText(hdc, 24, 190, "文字サイズ", app.hFont, clrMutedText) drawPaneText(hdc, 24, 190, "更新間隔", app.hFont, clrMutedText)
drawSettingsRefreshControl(hdc, app) drawSettingsRefreshControl(hdc, app)
drawPaneText(hdc, 24, 314, "文字サイズ", app.hFont, clrMutedText)
drawSettingsFontControl(hdc, app) drawSettingsFontControl(hdc, app)
drawPaneText(hdc, 24, 390, "履歴 Export", app.hFont, clrMutedText) drawPaneText(hdc, 24, 400, "履歴 Export", app.hFont, clrMutedText)
drawPaneText(hdc, 24, 414, "出力先フォルダ", app.hFont, clrMutedText) drawPaneText(hdc, 24, 430, "出力先フォルダ", app.hFont, clrMutedText)
drawSettingsBox(hdc, settingsHistoryExportDirRect(), 0x0038281a, clrPaneBorder)
drawPaneText(hdc, 120, 414, exportDirDisplayText(app.historyExportDir), app.hFont, clrText)
drawSettingsWideButton(hdc, settingsHistoryExportBrowseRect(), "参照", "", app.hFont, true)
drawSettingsWideButton(hdc, settingsHistoryExportTimeRect(), "Time", onOffBool(app.historyExportIncludeTime), app.hFont, app.historyExportIncludeTime) drawSettingsWideButton(hdc, settingsHistoryExportTimeRect(), "Time", onOffBool(app.historyExportIncludeTime), app.hFont, app.historyExportIncludeTime)
drawSettingsWideButton(hdc, settingsHistoryExportWorldRect(), "World", onOffBool(app.historyExportIncludeWorld), app.hFont, app.historyExportIncludeWorld) drawSettingsWideButton(hdc, settingsHistoryExportWorldRect(), "World", onOffBool(app.historyExportIncludeWorld), app.hFont, app.historyExportIncludeWorld)
drawSettingsWideButton(hdc, settingsHistoryExportJoinLeaveRect(), "Join/Leave", onOffBool(app.historyExportIncludeJoinLeave), app.hFont, app.historyExportIncludeJoinLeave) drawSettingsWideButton(hdc, settingsHistoryExportJoinLeaveRect(), "Join/Leave", onOffBool(app.historyExportIncludeJoinLeave), app.hFont, app.historyExportIncludeJoinLeave)
drawSettingsWideButton(hdc, settingsHistoryExportCustomToggleRect(), "Custom", onOffBool(app.historyExportCustomEnabled), app.hFont, app.historyExportCustomEnabled) drawSettingsWideButton(hdc, settingsHistoryExportCustomToggleRect(), "Custom", onOffBool(app.historyExportCustomEnabled), app.hFont, app.historyExportCustomEnabled)
drawPaneText(hdc, 24, 522, "Custom 条件", app.hFont, clrMutedText) drawPaneText(hdc, 24, 618, "Custom 条件", app.hFont, clrMutedText)
drawSettingsBox(hdc, settingsHistoryExportCustomRect(), 0x0038281a, clrPaneBorder)
drawPaneText(hdc, 120, 522, previewLine(app.historyExportCustom, 60), app.hFont, clrText)
drawSettingsWideButton(hdc, settingsHistoryExportCustomSaveRect(), "保存", "", app.hFont, true)
return 0 return 0
} }
@@ -1870,35 +2062,35 @@ func settingsFontUpRect() winRect {
} }
func settingsHistoryExportDirRect() winRect { func settingsHistoryExportDirRect() winRect {
return winRect{Left: 116, Top: 410, Right: 636, Bottom: 434} return winRect{Left: 24, Top: 458, Right: 636, Bottom: 490}
} }
func settingsHistoryExportBrowseRect() winRect { func settingsHistoryExportBrowseRect() winRect {
return winRect{Left: 644, Top: 410, Right: 736, Bottom: 434} return winRect{Left: 644, Top: 458, Right: 736, Bottom: 490}
} }
func settingsHistoryExportTimeRect() winRect { func settingsHistoryExportTimeRect() winRect {
return winRect{Left: 24, Top: 442, Right: 360, Bottom: 476} return winRect{Left: 24, Top: 510, Right: 360, Bottom: 546}
} }
func settingsHistoryExportWorldRect() winRect { func settingsHistoryExportWorldRect() winRect {
return winRect{Left: 376, Top: 442, Right: 736, Bottom: 476} return winRect{Left: 376, Top: 510, Right: 736, Bottom: 546}
} }
func settingsHistoryExportJoinLeaveRect() winRect { func settingsHistoryExportJoinLeaveRect() winRect {
return winRect{Left: 24, Top: 480, Right: 360, Bottom: 514} return winRect{Left: 24, Top: 558, Right: 360, Bottom: 594}
} }
func settingsHistoryExportCustomToggleRect() winRect { func settingsHistoryExportCustomToggleRect() winRect {
return winRect{Left: 376, Top: 480, Right: 736, Bottom: 514} return winRect{Left: 376, Top: 558, Right: 736, Bottom: 594}
} }
func settingsHistoryExportCustomRect() winRect { func settingsHistoryExportCustomRect() winRect {
return winRect{Left: 116, Top: 520, Right: 636, Bottom: 544} return winRect{Left: 24, Top: 646, Right: 636, Bottom: 678}
} }
func settingsHistoryExportCustomSaveRect() winRect { func settingsHistoryExportCustomSaveRect() winRect {
return winRect{Left: 644, Top: 520, Right: 736, Bottom: 544} return winRect{Left: 644, Top: 646, Right: 736, Bottom: 678}
} }
func handleSettingsPaneClick(app *guiApp, x, y int32, getWindowText, setWindowText *syscall.LazyProc) bool { func handleSettingsPaneClick(app *guiApp, x, y int32, getWindowText, setWindowText *syscall.LazyProc) bool {
@@ -1929,15 +2121,21 @@ func handleSettingsPaneClick(app *guiApp, x, y int32, getWindowText, setWindowTe
applyRefreshTimer(app.hwnd, app) applyRefreshTimer(app.hwnd, app)
} }
case pointInRect(x, y, settingsFontDownRect()): case pointInRect(x, y, settingsFontDownRect()):
if app.fontSize > 10 { if app.fontSize > minGUIFontSize {
app.fontSize -= 2 app.fontSize -= 2
if app.fontSize < minGUIFontSize {
app.fontSize = minGUIFontSize
}
app.hFont = createAppFont(app.fontSize) app.hFont = createAppFont(app.fontSize)
applyFont(app) applyFont(app)
saveGUISettings(app) saveGUISettings(app)
} }
case pointInRect(x, y, settingsFontUpRect()): case pointInRect(x, y, settingsFontUpRect()):
if app.fontSize < 30 { if app.fontSize < maxGUIFontSize {
app.fontSize += 2 app.fontSize += 2
if app.fontSize > maxGUIFontSize {
app.fontSize = maxGUIFontSize
}
app.hFont = createAppFont(app.fontSize) app.hFont = createAppFont(app.fontSize)
applyFont(app) applyFont(app)
saveGUISettings(app) saveGUISettings(app)
@@ -1982,9 +2180,9 @@ func drawSettingsRefreshControl(hdc uintptr, app *guiApp) {
drawSettingsBox(hdc, settingsRefreshValueRect(), uint32(0x0038281a), border) drawSettingsBox(hdc, settingsRefreshValueRect(), uint32(0x0038281a), border)
drawSettingsBox(hdc, settingsRefreshUpRect(), fill, border) drawSettingsBox(hdc, settingsRefreshUpRect(), fill, border)
drawSettingsWideButton(hdc, settingsRefreshUnitRect(), "\u5358\u4f4d", refreshIntervalUnitLabel(app.refreshIntervalUnit), font, true) drawSettingsWideButton(hdc, settingsRefreshUnitRect(), "\u5358\u4f4d", refreshIntervalUnitLabel(app.refreshIntervalUnit), font, true)
drawCenteredPaneText(hdc, settingsRefreshDownRect(), "A-", font, clrText) drawCenteredPaneText(hdc, settingsRefreshDownRect(), "-", font, clrText)
drawCenteredPaneText(hdc, settingsRefreshValueRect(), refreshIntervalValueLabel(app.refreshIntervalValue, app.refreshIntervalUnit), font, clrMutedText) drawCenteredPaneText(hdc, settingsRefreshValueRect(), refreshIntervalValueLabel(app.refreshIntervalValue, app.refreshIntervalUnit), font, clrMutedText)
drawCenteredPaneText(hdc, settingsRefreshUpRect(), "A+", font, clrText) drawCenteredPaneText(hdc, settingsRefreshUpRect(), "+", font, clrText)
} }
func refreshIntervalValueLabel(value int, unit string) string { func refreshIntervalValueLabel(value int, unit string) string {
@@ -2609,7 +2807,7 @@ func drawSettingsFontControl(hdc uintptr, app *guiApp) {
drawSettingsBox(hdc, settingsFontValueRect(), uint32(0x0038281a), border) drawSettingsBox(hdc, settingsFontValueRect(), uint32(0x0038281a), border)
drawSettingsBox(hdc, settingsFontUpRect(), fill, border) drawSettingsBox(hdc, settingsFontUpRect(), fill, border)
drawCenteredPaneText(hdc, settingsFontDownRect(), "A-", font, clrText) drawCenteredPaneText(hdc, settingsFontDownRect(), "A-", font, clrText)
drawCenteredPaneText(hdc, settingsFontValueRect(), strconv.Itoa(app.fontSize), font, clrMutedText) drawCenteredPaneText(hdc, settingsFontValueRect(), strconv.Itoa(app.fontSize)+" pt", font, clrMutedText)
drawCenteredPaneText(hdc, settingsFontUpRect(), "A+", font, clrText) drawCenteredPaneText(hdc, settingsFontUpRect(), "A+", font, clrText)
} }
@@ -2892,7 +3090,7 @@ func eventRows(users []userState) []guiEventRow {
} }
func joinLeaveRowsForCurrentWorld(app *guiApp) []guiEventRow { func joinLeaveRowsForCurrentWorld(app *guiApp) []guiEventRow {
events := historyEventsFromSnapshot() events := historyEventsWithFallback()
if len(events) == 0 { if len(events) == 0 {
users, _ := currentUsersFromJoinLeave() users, _ := currentUsersFromJoinLeave()
return eventRows(users) return eventRows(users)
@@ -3242,7 +3440,7 @@ func humanDurationLabel(started, ended time.Time, current bool) string {
} }
d := ended.Sub(started) d := ended.Sub(started)
if current { if current {
return formatDurationShort(d) + " active" return formatDurationShort(d)
} }
return formatDurationShort(d) return formatDurationShort(d)
} }
@@ -3267,7 +3465,7 @@ func formatDurationShort(d time.Duration) string {
func joinLogFontSize(baseSize, presentCount int) int { func joinLogFontSize(baseSize, presentCount int) int {
size := baseSize size := baseSize
if size <= 0 { if size <= 0 {
size = 18 size = defaultGUIFontSize
} }
if presentCount <= 32 { if presentCount <= 32 {
return size return size
@@ -3280,8 +3478,8 @@ func joinLogFontSize(baseSize, presentCount int) int {
case presentCount >= 33: case presentCount >= 33:
size -= 2 size -= 2
} }
if size < 8 { if size < minGUIFontSize {
size = 8 size = minGUIFontSize
} }
return size return size
} }
@@ -3736,7 +3934,7 @@ func loadGUISettings() config.GUIConfig {
if err != nil || cfg == nil { if err != nil || cfg == nil {
return config.GUIConfig{ return config.GUIConfig{
TopMost: false, TopMost: false,
FontSize: 18, FontSize: defaultGUIFontSize,
RefreshIntervalValue: 2, RefreshIntervalValue: 2,
RefreshIntervalUnit: "sec", RefreshIntervalUnit: "sec",
HistoryExportIncludeTime: true, HistoryExportIncludeTime: true,
@@ -3745,7 +3943,7 @@ func loadGUISettings() config.GUIConfig {
} }
} }
if cfg.GUI.FontSize <= 0 { if cfg.GUI.FontSize <= 0 {
cfg.GUI.FontSize = 18 cfg.GUI.FontSize = defaultGUIFontSize
} }
cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit = normalizeRefreshIntervalSettings(cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit) cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit = normalizeRefreshIntervalSettings(cfg.GUI.RefreshIntervalValue, cfg.GUI.RefreshIntervalUnit)
cfg.GUI.HistoryRegex = strings.TrimSpace(cfg.GUI.HistoryRegex) cfg.GUI.HistoryRegex = strings.TrimSpace(cfg.GUI.HistoryRegex)
@@ -4096,7 +4294,7 @@ func formatRightPane(tab string, state map[string]any, worldLabel string, curren
b.WriteString("\r\nFont size: ") b.WriteString("\r\nFont size: ")
fontSize, _ := state["font_size"].(int) fontSize, _ := state["font_size"].(int)
if fontSize == 0 { if fontSize == 0 {
fontSize = 18 fontSize = defaultGUIFontSize
} }
b.WriteString(strconv.Itoa(fontSize)) b.WriteString(strconv.Itoa(fontSize))
b.WriteString("\r\nLeave unmute: ") b.WriteString("\r\nLeave unmute: ")
@@ -4134,7 +4332,7 @@ func formatSettingsPane(state map[string]any) string {
b.WriteString("\r\nFont size: ") b.WriteString("\r\nFont size: ")
fontSize, _ := state["font_size"].(int) fontSize, _ := state["font_size"].(int)
if fontSize == 0 { if fontSize == 0 {
fontSize = 18 fontSize = defaultGUIFontSize
} }
b.WriteString(strconv.Itoa(fontSize)) b.WriteString(strconv.Itoa(fontSize))
b.WriteString("\r\nLeave unmute: ") b.WriteString("\r\nLeave unmute: ")

View File

@@ -0,0 +1,7 @@
//go:build !windows
package app
func decodeWithCodePage(_ []byte, _ uint32) string {
return ""
}

View File

@@ -0,0 +1,37 @@
//go:build windows
package app
import (
"syscall"
"unsafe"
)
func decodeWithCodePage(b []byte, codePage uint32) string {
if len(b) == 0 {
return ""
}
kernel32 := syscall.NewLazyDLL("kernel32.dll")
multiByteToWideChar := kernel32.NewProc("MultiByteToWideChar")
n, _, _ := multiByteToWideChar.Call(
uintptr(codePage),
0,
uintptr(unsafe.Pointer(&b[0])),
uintptr(len(b)),
0,
0,
)
if n == 0 {
return ""
}
buf := make([]uint16, n)
multiByteToWideChar.Call(
uintptr(codePage),
0,
uintptr(unsafe.Pointer(&b[0])),
uintptr(len(b)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)),
)
return utf16ToString(buf)
}

View File

@@ -9,9 +9,8 @@ import (
"path/filepath" "path/filepath"
"regexp" "regexp"
"strings" "strings"
"syscall"
"time" "time"
"unsafe" "unicode/utf16"
) )
var ( var (
@@ -315,14 +314,14 @@ func decodeVRChatLog(b []byte) string {
for i := 2; i+1 < len(b); i += 2 { for i := 2; i+1 < len(b); i += 2 {
u16 = append(u16, uint16(b[i])|uint16(b[i+1])<<8) u16 = append(u16, uint16(b[i])|uint16(b[i+1])<<8)
} }
return strings.TrimPrefix(fixMojibake(syscall.UTF16ToString(u16)), "\ufeff") return strings.TrimPrefix(fixMojibake(utf16ToString(u16)), "\ufeff")
} }
if b[0] == 0xfe && b[1] == 0xff { if b[0] == 0xfe && b[1] == 0xff {
u16 := make([]uint16, 0, (len(b)-2)/2) u16 := make([]uint16, 0, (len(b)-2)/2)
for i := 2; i+1 < len(b); i += 2 { for i := 2; i+1 < len(b); i += 2 {
u16 = append(u16, uint16(b[i+1])|uint16(b[i])<<8) u16 = append(u16, uint16(b[i+1])|uint16(b[i])<<8)
} }
return strings.TrimPrefix(fixMojibake(syscall.UTF16ToString(u16)), "\ufeff") return strings.TrimPrefix(fixMojibake(utf16ToString(u16)), "\ufeff")
} }
} }
lines := bytes.Split(b, []byte{'\n'}) lines := bytes.Split(b, []byte{'\n'})
@@ -377,6 +376,16 @@ func tryRepairShiftJISMojibake(s string) string {
return "" return ""
} }
func utf16ToString(u16 []uint16) string {
for i, r := range u16 {
if r == 0 {
u16 = u16[:i]
break
}
}
return string(utf16.Decode(u16))
}
func scoreReadable(s string) int { func scoreReadable(s string) int {
if s == "" { if s == "" {
return -1 return -1
@@ -401,35 +410,6 @@ func scoreReadable(s string) int {
return score return score
} }
func decodeWithCodePage(b []byte, codePage uint32) string {
if len(b) == 0 {
return ""
}
kernel32 := syscall.NewLazyDLL("kernel32.dll")
multiByteToWideChar := kernel32.NewProc("MultiByteToWideChar")
n, _, _ := multiByteToWideChar.Call(
uintptr(codePage),
0,
uintptr(unsafe.Pointer(&b[0])),
uintptr(len(b)),
0,
0,
)
if n == 0 {
return ""
}
buf := make([]uint16, n)
multiByteToWideChar.Call(
uintptr(codePage),
0,
uintptr(unsafe.Pointer(&b[0])),
uintptr(len(b)),
uintptr(unsafe.Pointer(&buf[0])),
uintptr(len(buf)),
)
return syscall.UTF16ToString(buf)
}
func updateWorldState(state *vrcLogState, line string) (bool, string) { func updateWorldState(state *vrcLogState, line string) (bool, string) {
worldID := "" worldID := ""
instanceID := "" instanceID := ""

View File

@@ -53,7 +53,7 @@ func Load(path string) (*Config, error) {
cfg := &Config{ cfg := &Config{
OSC: OSCConfig{Host: "127.0.0.1", Port: 9001}, OSC: OSCConfig{Host: "127.0.0.1", Port: 9001},
GUI: GUIConfig{ GUI: GUIConfig{
FontSize: 18, FontSize: 14,
RefreshIntervalValue: 2, RefreshIntervalValue: 2,
RefreshIntervalUnit: "sec", RefreshIntervalUnit: "sec",
HistoryExportIncludeTime: true, HistoryExportIncludeTime: true,