Improve GUI guest sorting and presence display
This commit is contained in:
@@ -29,16 +29,19 @@ func Run(configPath string, mode string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
SetGuestTracker(NewGuestTracker(cfg.VrcLog.GuestNames))
|
||||
log.Printf("vrc_osc_go starting osc=%s:%d config=%s", cfg.OSC.Host, cfg.OSC.Port, configPath)
|
||||
|
||||
server := osc.NewServer(cfg.OSC.Host, cfg.OSC.Port)
|
||||
discordState := &discordMuteState{muted: true}
|
||||
SetDiscordMuted(true)
|
||||
server.Map("DiscordSend", func(_ string, args []osc.Value) error {
|
||||
log.Printf("received DiscordSend args=%+v", args)
|
||||
if err := handleDiscordSend(discordState, args); err != nil {
|
||||
log.Printf("discord mute failed: %v", err)
|
||||
return err
|
||||
}
|
||||
SetDiscordMuted(discordState.muted)
|
||||
return nil
|
||||
})
|
||||
server.Map("ocrEnabled", func(_ string, args []osc.Value) error {
|
||||
|
||||
@@ -36,6 +36,7 @@ func handleDiscordSend(state *discordMuteState, args []osc.Value) error {
|
||||
return err
|
||||
}
|
||||
state.muted = false
|
||||
SetDiscordMuted(state.muted)
|
||||
state.lastAction = now
|
||||
} else {
|
||||
log.Printf("INFO already unmuted")
|
||||
@@ -51,6 +52,7 @@ func handleDiscordSend(state *discordMuteState, args []osc.Value) error {
|
||||
return err
|
||||
}
|
||||
state.muted = true
|
||||
SetDiscordMuted(state.muted)
|
||||
state.lastAction = now
|
||||
} else {
|
||||
log.Printf("INFO already muted")
|
||||
|
||||
109
VRWT_Tool/VRC_OSC/internal/app/guest_status.go
Normal file
109
VRWT_Tool/VRC_OSC/internal/app/guest_status.go
Normal file
@@ -0,0 +1,109 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type GuestStatus struct {
|
||||
Name string `json:"name"`
|
||||
Present bool `json:"present"`
|
||||
LastJoin time.Time `json:"last_join"`
|
||||
LastLeave time.Time `json:"last_leave"`
|
||||
AbsentFor string `json:"absent_for"`
|
||||
}
|
||||
|
||||
type GuestTracker struct {
|
||||
mu sync.RWMutex
|
||||
current string
|
||||
items map[string]*GuestStatus
|
||||
}
|
||||
|
||||
func NewGuestTracker(names []string) *GuestTracker {
|
||||
items := make(map[string]*GuestStatus, len(names))
|
||||
for _, name := range names {
|
||||
items[name] = &GuestStatus{Name: name}
|
||||
}
|
||||
return &GuestTracker{items: items}
|
||||
}
|
||||
|
||||
func (t *GuestTracker) SetCurrentInstance(label string) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
t.current = label
|
||||
}
|
||||
|
||||
func (t *GuestTracker) CurrentInstance() string {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
return t.current
|
||||
}
|
||||
|
||||
func (t *GuestTracker) MarkJoin(name string, at time.Time) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
s := t.ensure(name)
|
||||
s.Present = true
|
||||
s.LastJoin = at
|
||||
s.AbsentFor = ""
|
||||
}
|
||||
|
||||
func (t *GuestTracker) MarkLeave(name string, at time.Time) {
|
||||
t.mu.Lock()
|
||||
defer t.mu.Unlock()
|
||||
s := t.ensure(name)
|
||||
s.Present = false
|
||||
s.LastLeave = at
|
||||
if !s.LastJoin.IsZero() {
|
||||
s.AbsentFor = humanSince(at)
|
||||
}
|
||||
}
|
||||
|
||||
func (t *GuestTracker) Snapshot(now time.Time) []GuestStatus {
|
||||
t.mu.RLock()
|
||||
defer t.mu.RUnlock()
|
||||
out := make([]GuestStatus, 0, len(t.items))
|
||||
for _, s := range t.items {
|
||||
cp := *s
|
||||
if !cp.Present && !cp.LastLeave.IsZero() {
|
||||
cp.AbsentFor = humanSince(cp.LastLeave, now)
|
||||
}
|
||||
out = append(out, cp)
|
||||
}
|
||||
sort.Slice(out, func(i, j int) bool { return out[i].Name < out[j].Name })
|
||||
return out
|
||||
}
|
||||
|
||||
func (t *GuestTracker) ensure(name string) *GuestStatus {
|
||||
if s, ok := t.items[name]; ok {
|
||||
return s
|
||||
}
|
||||
s := &GuestStatus{Name: name}
|
||||
t.items[name] = s
|
||||
return s
|
||||
}
|
||||
|
||||
func humanSince(at time.Time, now ...time.Time) string {
|
||||
ref := time.Now()
|
||||
if len(now) > 0 {
|
||||
ref = now[0]
|
||||
}
|
||||
if at.IsZero() {
|
||||
return ""
|
||||
}
|
||||
d := ref.Sub(at)
|
||||
if d < time.Minute {
|
||||
return "1分未満"
|
||||
}
|
||||
m := int(d.Minutes())
|
||||
if m < 60 {
|
||||
return fmt.Sprintf("%d分前", m)
|
||||
}
|
||||
h := m / 60
|
||||
if h < 24 {
|
||||
return fmt.Sprintf("%d時間前", h)
|
||||
}
|
||||
return fmt.Sprintf("%d日前", h/24)
|
||||
}
|
||||
@@ -20,6 +20,8 @@ runOcrFromScreen()
|
||||
out, err := cmd.CombinedOutput()
|
||||
if len(out) > 0 {
|
||||
log.Printf("ocr output: %s", string(out))
|
||||
SetOCRText(string(out))
|
||||
SetTranslateText("翻訳: 未実装")
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("ocr failed: %w", err)
|
||||
|
||||
47
VRWT_Tool/VRC_OSC/internal/app/state.go
Normal file
47
VRWT_Tool/VRC_OSC/internal/app/state.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package app
|
||||
|
||||
import "sync"
|
||||
|
||||
type RuntimeState struct {
|
||||
mu sync.RWMutex
|
||||
DiscordMuted bool
|
||||
LastOCRText string
|
||||
LastTranslate string
|
||||
CurrentWorld string
|
||||
}
|
||||
|
||||
var runtimeState = &RuntimeState{}
|
||||
var guestTracker *GuestTracker
|
||||
|
||||
func SetGuestTracker(t *GuestTracker) { guestTracker = t }
|
||||
func GetGuestTracker() *GuestTracker { return guestTracker }
|
||||
|
||||
func SetDiscordMuted(v bool) {
|
||||
runtimeState.mu.Lock()
|
||||
defer runtimeState.mu.Unlock()
|
||||
runtimeState.DiscordMuted = v
|
||||
}
|
||||
|
||||
func SetOCRText(v string) {
|
||||
runtimeState.mu.Lock()
|
||||
defer runtimeState.mu.Unlock()
|
||||
runtimeState.LastOCRText = v
|
||||
}
|
||||
|
||||
func SetTranslateText(v string) {
|
||||
runtimeState.mu.Lock()
|
||||
defer runtimeState.mu.Unlock()
|
||||
runtimeState.LastTranslate = v
|
||||
}
|
||||
|
||||
func SetCurrentWorld(v string) {
|
||||
runtimeState.mu.Lock()
|
||||
defer runtimeState.mu.Unlock()
|
||||
runtimeState.CurrentWorld = v
|
||||
}
|
||||
|
||||
func SnapshotRuntime() RuntimeState {
|
||||
runtimeState.mu.RLock()
|
||||
defer runtimeState.mu.RUnlock()
|
||||
return *runtimeState
|
||||
}
|
||||
@@ -128,11 +128,19 @@ func watchVrchatLog() {
|
||||
if changed, worldLabel := updateWorldState(state, line); changed {
|
||||
log.Printf("world changed: %s", worldLabel)
|
||||
state.presentSet = map[string]struct{}{}
|
||||
SetCurrentWorld(worldLabel)
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.SetCurrentInstance(worldLabel)
|
||||
}
|
||||
_ = appendRuntimeLog("VRC WORLD", worldLabel)
|
||||
}
|
||||
at := extractLineTime(line)
|
||||
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
state.presentSet[name] = struct{}{}
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.MarkJoin(name, at)
|
||||
}
|
||||
out := fmt.Sprintf("[join] %s (%d)", name, len(state.presentSet))
|
||||
log.Print(out)
|
||||
_ = appendJoinLeaveLog(out)
|
||||
@@ -141,6 +149,9 @@ func watchVrchatLog() {
|
||||
if m := leftPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
delete(state.presentSet, name)
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.MarkLeave(name, at)
|
||||
}
|
||||
out := fmt.Sprintf("[leave] %s (%d)", name, len(state.presentSet))
|
||||
log.Print(out)
|
||||
_ = appendJoinLeaveLog(out)
|
||||
@@ -178,11 +189,17 @@ func scanExistingVrchatLog(path string, state *vrcLogState) error {
|
||||
if m := joinPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
state.presentSet[name] = struct{}{}
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.MarkJoin(name, extractLineTime(line))
|
||||
}
|
||||
continue
|
||||
}
|
||||
if m := leftPattern.FindStringSubmatch(line); len(m) == 2 {
|
||||
name := strings.TrimSpace(m[1])
|
||||
delete(state.presentSet, name)
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.MarkLeave(name, extractLineTime(line))
|
||||
}
|
||||
continue
|
||||
}
|
||||
}
|
||||
@@ -192,6 +209,10 @@ func scanExistingVrchatLog(path string, state *vrcLogState) error {
|
||||
state.initialized = true
|
||||
if state.worldName != "" {
|
||||
log.Printf("world changed: %s", state.worldName)
|
||||
SetCurrentWorld(state.worldName)
|
||||
if tracker := GetGuestTracker(); tracker != nil {
|
||||
tracker.SetCurrentInstance(state.worldName)
|
||||
}
|
||||
_ = appendRuntimeLog("VRC WORLD", state.worldName)
|
||||
}
|
||||
return nil
|
||||
@@ -311,3 +332,14 @@ func shouldUseRoomTitlePreferLatest(roomTitle, currentWorldName string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func extractLineTime(line string) time.Time {
|
||||
if len(line) < 19 {
|
||||
return time.Now()
|
||||
}
|
||||
t, err := time.ParseInLocation("2006.01.02 15:04:05", line[:19], time.Local)
|
||||
if err == nil {
|
||||
return t
|
||||
}
|
||||
return time.Now()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user