Improve GUI guest sorting and presence display

This commit is contained in:
every_holiday
2026-06-24 02:38:06 +09:00
parent b6421958af
commit ab9551c241
8 changed files with 398 additions and 1 deletions

View File

@@ -0,0 +1,174 @@
package main
import (
"encoding/json"
"fmt"
"log"
"net/http"
"os/exec"
"runtime"
"time"
"vrc_osc_go/internal/app"
)
func main() {
go func() {
if err := app.Run("config/config.toml", ""); err != nil {
log.Printf("backend stopped: %v", err)
}
}()
mux := http.NewServeMux()
mux.HandleFunc("/", handleIndex)
mux.HandleFunc("/api/state", handleState)
addr := "127.0.0.1:18181"
go openBrowser("http://" + addr)
log.Printf("gui listening on http://%s", addr)
log.Fatal(http.ListenAndServe(addr, mux))
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
html := `<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="refresh" content="3">
<title>VRC_OSC GUI</title>
<style>
body{font-family:sans-serif;background:#111;color:#eee;margin:20px}
.grid{display:grid;grid-template-columns:1fr 1fr;gap:16px}
.card{background:#1b1b1b;border:1px solid #333;border-radius:12px;padding:16px}
table{width:100%;border-collapse:collapse}
td,th{border-bottom:1px solid #333;padding:6px;text-align:left}
th{cursor:pointer;user-select:none}
th:hover{color:#8ff}
.on{color:#5ff28f}.off{color:#ff8a8a}
</style>
</head>
<body>
<h1>VRC_OSC GUI</h1>
<div id="root">loading...</div>
<script>
const sortKey = 'vrc_osc_gui_sort';
const defaultSort = { key: 'name', dir: 1 };
let sortState = (() => {
try {
const raw = localStorage.getItem(sortKey);
return raw ? JSON.parse(raw) : defaultSort;
} catch (_) {
return defaultSort;
}
})();
function setSort(key) {
if (sortState.key === key) {
sortState.dir = sortState.dir * -1;
} else {
sortState = { key: key, dir: 1 };
}
localStorage.setItem(sortKey, JSON.stringify(sortState));
load();
}
function compareGuests(a, b) {
if (sortState.key === 'name') {
const v = a.name.localeCompare(b.name, 'ja');
return v * sortState.dir;
}
if (sortState.key === 'presence') {
const av = Number(a.present);
const bv = Number(b.present);
if (av !== bv) {
return (bv - av) * sortState.dir;
}
return a.name.localeCompare(b.name, 'ja');
}
if (sortState.key === 'status') {
const av = Number(!a.present);
const bv = Number(!b.present);
if (av !== bv) {
return (av - bv) * sortState.dir;
}
return a.name.localeCompare(b.name, 'ja');
}
return a.name.localeCompare(b.name, 'ja');
}
async function load(){
const r = await fetch('/api/state');
const j = await r.json();
const guests = [...j.guests].sort(compareGuests);
const presentCount = guests.filter(g => g.present).length;
const rows = guests.map(g => {
const status = g.present ? '在席' : (g.absent_for || '1分未満');
return '<tr><td>' + esc(g.name) + '</td><td>' + esc(status) + '</td></tr>';
}).join('');
document.getElementById('root').innerHTML =
'<div class="grid">' +
'<div class="card">' +
'<h2>状態</h2>' +
'<p>World: ' + esc(j.world || '') + '</p>' +
'<p>Discord: <span class="' + (j.discord_muted ? 'on' : 'off') + '">' + (j.discord_muted ? 'Muted' : 'Unmuted') + '</span></p>' +
'<p>OCR: ' + esc(j.ocr || '') + '</p>' +
'<p>翻訳: ' + esc(j.translate || '未実装') + '</p>' +
'</div>' +
'<div class="card">' +
'<h2>ゲスト (' + presentCount + '/' + j.guest_count + ')</h2>' +
'<table>' +
'<tr>' +
'<th data-sort="name">名前' + sortMark('name') + '</th>' +
'<th data-sort="presence">在席' + sortMark('presence') + '</th>' +
'</tr>' +
rows +
'</table>' +
'</div>' +
'</div>';
document.querySelectorAll('th[data-sort]').forEach(function(el) {
el.style.cursor = 'pointer';
el.onclick = function() {
setSort(el.getAttribute('data-sort'));
};
});
}
function sortMark(key) {
if (sortState.key !== key) return '';
return sortState.dir > 0 ? ' ▲' : ' ▼';
}
function esc(s){ return (s || '').replace(/[&<>"]/g, function(m){ return {'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;'}[m]; }); }
setInterval(load, 3000);
load();
</script>
</body>
</html>`
_, _ = fmt.Fprint(w, html)
}
func handleState(w http.ResponseWriter, r *http.Request) {
s := app.SnapshotRuntime()
guests := []app.GuestStatus{}
if t := app.GetGuestTracker(); t != nil {
guests = t.Snapshot(time.Now())
}
_ = json.NewEncoder(w).Encode(map[string]any{
"world": s.CurrentWorld,
"discord_muted": s.DiscordMuted,
"ocr": s.LastOCRText,
"translate": s.LastTranslate,
"guests": guests,
"guest_count": len(guests),
})
}
func openBrowser(url string) {
switch runtime.GOOS {
case "windows":
_ = exec.Command("cmd", "/c", "start", "", url).Start()
default:
_ = exec.Command("xdg-open", url).Start()
}
}