Add a settings page for profit display and default stack

- Per-device toggle to hide your own net and the net ranking at tables
- Default starting stack set in settings; creating a table saves the
  stack, SB and BB used as the next defaults
- Number fields on create and settings select all on focus

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-23 12:31:32 +09:00
1 parent ed3d1e437e
commit 22ee28ba03
9 files changed
+160 -8

No files matched your search

+39 -1
View File
@@ -28,8 +28,46 @@
}
return t;
},
// テーブルで自分の収支を表示するか(設定画面で切り替える。既定は表示)
get showNet() { return lsGet('poker.showNet') !== '0'; },
set showNet(v) { lsSet('poker.showNet', v ? '1' : '0'); },
// テーブル作成画面の開始スタックの初期値(設定画面で変えられ、テーブルを作るたびに前回値で上書きする)
get defaultStack() {
const n = Number(lsGet('poker.defaultStack'));
return Number.isInteger(n) && n >= 10 && n <= 100000000 ? n : 1000;
},
set defaultStack(v) { lsSet('poker.defaultStack', String(v)); },
// 前回テーブルを作ったときの SB/BB と、そのときの開始スタック { stack, sb, bb }
get lastBlinds() {
try {
const b = JSON.parse(lsGet('poker.lastBlinds'));
return b && [b.stack, b.sb, b.bb].every(Number.isInteger) && b.sb >= 1 && b.bb > b.sb && b.bb <= b.stack ? b : null;
} catch {
return null;
}
},
set lastBlinds(v) { lsSet('poker.lastBlinds', JSON.stringify(v)); },
};
// 数値の入力欄: フォーカスしたら全体を選択して、そのまま打ち替えられるようにする
function selectOnFocus(el) {
let skipMouseUp = false;
el.addEventListener('focus', () => {
el.select();
skipMouseUp = true;
// iOS Safari はタップの処理の後で選択が外れるので、もう一度選ぶ
setTimeout(() => {
if (document.activeElement === el) el.select();
}, 0);
});
// クリックで入ったときは、離した位置にカーソルが置かれて選択が外れるのを防ぐ
el.addEventListener('mouseup', (e) => {
if (skipMouseUp) e.preventDefault();
skipMouseUp = false;
});
el.addEventListener('keydown', () => { skipMouseUp = false; });
}
function esc(s) {
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[c]));
}
@@ -233,5 +271,5 @@
FINISHING: 'FINISHING',
};
window.App = { Store, esc, chips, toast, confirm: confirmDialog, api, copyText, connectSocket, safeNext, PHASE_LABELS };
window.App = { Store, esc, chips, toast, selectOnFocus, confirm: confirmDialog, api, copyText, connectSocket, safeNext, PHASE_LABELS };
})();
+13 -1
View File
@@ -1,7 +1,7 @@
(function () {
'use strict';
const { Store, api, chips } = App;
const { Store, api, chips, selectOnFocus } = App;
if (!Store.name) {
location.replace('/');
return;
@@ -109,6 +109,7 @@
timerInput.addEventListener('change', refresh);
intervalInput.addEventListener('input', refresh);
form.querySelectorAll('input[type="radio"]').forEach((el) => el.addEventListener('change', refresh));
[stackInput, sbInput, bbInput, intervalInput].forEach(selectOnFocus);
$('reroll').addEventListener('click', suggestName);
Fluid.Segmented(form.querySelector('.segmented.numbers'));
Fluid.navBar($('nav'), $('large-title'));
@@ -152,6 +153,9 @@
cpuCount: Number(radioValue('cpu-count')),
token: Store.token,
});
// 次にテーブルを作るときの初期値にする
Store.defaultStack = int(stackInput);
Store.lastBlinds = { stack: int(stackInput), sb: int(sbInput), bb: int(bbInput) };
location.href = '/table/' + encodeURIComponent(id);
} catch (err) {
errorEl.textContent = err.message;
@@ -164,7 +168,15 @@
.then((me) => { $('review-mode').hidden = !me.admin; })
.catch(() => {});
// 開始スタックは設定(=前回値)。SB/BB は前回と同じ開始スタックなら前回値を引き継ぐ
stackInput.value = Store.defaultStack;
applyDefaultBlinds();
const last = Store.lastBlinds;
if (last && last.stack === Store.defaultStack && (last.sb !== int(sbInput) || last.bb !== int(bbInput))) {
sbInput.value = last.sb;
bbInput.value = last.bb;
blindsTouched = true;
}
refresh();
suggestName();
})();
+34
View File
@@ -0,0 +1,34 @@
// 設定: この端末だけに保存する(localStorage)
(function () {
'use strict';
const { Store, selectOnFocus } = App;
const $ = (id) => document.getElementById(id);
Fluid.navBar($('nav'), $('large-title'));
const showNet = $('show-net');
showNet.checked = Store.showNet;
showNet.addEventListener('change', () => {
Store.showNet = showNet.checked;
Fluid.haptic(6);
});
const stack = $('default-stack');
stack.value = Store.defaultStack;
selectOnFocus(stack);
// 正しい値のときだけ保存する。外したときに不正なら保存済みの値に戻す
stack.addEventListener('input', () => {
const n = Number(stack.value);
const ok = Number.isInteger(n) && n >= 10 && n <= 100000000;
stack.classList.toggle('invalid', stack.value !== '' && !ok);
if (ok) Store.defaultStack = n;
});
stack.addEventListener('blur', () => {
stack.classList.remove('invalid');
stack.value = Store.defaultStack;
});
stack.addEventListener('keydown', (e) => {
if (e.key === 'Enter') stack.blur();
});
})();
+3
View File
@@ -468,6 +468,9 @@ a {
.tone-purple {
background: linear-gradient(180deg, #c86ff0, #9440c2);
}
.tone-gray {
background: linear-gradient(180deg, #a1a1a8, #75757c);
}
/* セルの中の入力(ラベルが左、値が右) */
.field-cell {
+10 -5
View File
@@ -437,8 +437,9 @@
const o = m.options;
Fluid.countTo($('my-stack'), m.stack, chips);
const net = m.net;
$('my-net').textContent = `収支 ${net > 0 ? '+' : net < 0 ? '−' : '±'}${chips(Math.abs(net))}`;
// 収支は設定で消せる(この端末だけ)
const net = Store.showNet ? m.net : null;
$('my-net').textContent = net == null ? '' : `収支 ${net > 0 ? '+' : net < 0 ? '−' : '±'}${chips(Math.abs(net))}`;
$('my-net').className = net > 0 ? 'plus' : net < 0 ? 'minus' : '';
$('my-bet').textContent = seat.inHand && seat.bet > 0 ? `ベット中 ${chips(seat.bet)}` : '';
$('my-hand').textContent = (seat.inHand && seat.handName) || '';
@@ -642,8 +643,11 @@
net: '収支 = 持っているチップ − バイインの合計(リバイを含む)',
};
// 収支を表示しない設定のときはスタック順だけ
const rankMode = () => (Store.showNet ? rankBy : 'stack');
function rankingEntries(s) {
const key = rankBy === 'net' ? 'net' : 'held';
const key = rankMode() === 'net' ? 'net' : 'held';
const list = s.seats.filter(Boolean).sort((a, b) => b[key] - a[key] || a.seat - b.seat);
let rank = 0;
return list.map((p, i) => {
@@ -694,7 +698,7 @@
const sub = [p.seat === s.mySeat && 'あなた', p.bot && 'CPU', p.sittingOut && '離席中', !p.connected && 'オフライン']
.filter(Boolean)
.join(' ・ ');
const net = rankBy === 'net';
const net = rankMode() === 'net';
patch(
el,
`<span class="rank-no${rank <= 3 ? ` top-${rank}` : ''}">${rank}</span>` +
@@ -712,7 +716,8 @@
rankRows.delete(seat);
}
slideFrom(before); // 順位が変わった行は元の位置から滑らせる
$('ranking-note').textContent = RANK_NOTE[rankBy];
$('ranking-tabs').hidden = !Store.showNet;
$('ranking-note').textContent = RANK_NOTE[rankMode()];
if (!s.seats.some(Boolean)) listEl.innerHTML = '<li class="empty">まだ誰も座っていません</li>';
}
+1 -1
View File
@@ -88,7 +88,7 @@
<div class="list">
<div class="cell field-cell value-right">
<label for="stack">開始スタック</label>
<input id="stack" type="number" inputmode="numeric" min="10" step="1" value="1000">
<input id="stack" type="number" inputmode="numeric" min="10" step="1">
</div>
<div class="cell field-cell value-right">
<label for="sb">スモールブラインド</label>
+5
View File
@@ -38,6 +38,11 @@
<span class="cell-title">統計<span class="cell-sub">プレイスタイル・VPIP・PFR など</span></span>
<svg class="i chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
</a>
<a class="cell cell-link" href="/settings">
<span class="cell-icon tone-gray"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></svg></span>
<span class="cell-title">設定<span class="cell-sub">収支の表示・開始スタック</span></span>
<svg class="i chevron" viewBox="0 0 24 24" aria-hidden="true"><path d="M9 5l7 7-7 7"/></svg>
</a>
</div>
<div class="error-text" id="name-error" role="alert"></div>
</section>
+54
View File
@@ -0,0 +1,54 @@
<!doctype html>
<html lang="ja">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover">
<meta name="theme-color" media="(prefers-color-scheme: light)" content="#f2f2f7">
<meta name="theme-color" media="(prefers-color-scheme: dark)" content="#000000">
<meta name="robots" content="noindex, nofollow">
<title>設定 - Hold'em Night</title>
<link rel="icon" href="/assets/favicon.ico">
<link rel="stylesheet" href="/assets/style.css">
</head>
<body class="page-settings">
<header class="nav-bar" id="nav">
<div class="nav-inner">
<a class="nav-back" href="/"><svg class="i" viewBox="0 0 24 24" aria-hidden="true"><path d="M15 5l-7 7 7 7"/></svg>ロビー</a>
<span class="nav-title"></span>
</div>
</header>
<main class="wrap">
<h1 class="large-title" id="large-title">設定</h1>
<section class="group">
<div class="group-header">テーブル</div>
<div class="list">
<div class="cell">
<span class="cell-title" id="show-net-label">収支を表示</span>
<span class="switch">
<input id="show-net" type="checkbox" aria-labelledby="show-net-label">
<span></span>
</span>
</div>
</div>
<div class="group-footer">オフにすると、テーブルで自分の収支と、ランキングの「収支」が表示されなくなります。この端末だけの設定で、他の人の画面は変わりません。</div>
</section>
<section class="group">
<div class="group-header">テーブルの作成</div>
<div class="list">
<div class="cell field-cell value-right">
<label for="default-stack">開始スタック</label>
<input id="default-stack" type="number" inputmode="numeric" min="10" step="1">
</div>
</div>
<div class="group-footer" id="stack-hint">テーブルを作るときの初期値です。テーブルを作ると、そのときの値(SB・BB も)が次回の初期値になります。</div>
</section>
</main>
<script src="/assets/fluid.js"></script>
<script src="/assets/common.js"></script>
<script src="/assets/settings.js"></script>
</body>
</html>
+1
View File
@@ -92,6 +92,7 @@ app.get('/', page('lobby.html'));
app.get('/create', page('create.html'));
app.get('/table/:id', page('table.html'));
app.get('/stats', page('stats.html'));
app.get('/settings', page('settings.html'));
// ---------- 管理者専用: ハンドレンジ学習 ----------
app.get('/range', auth.requireAdmin, page('range.html', ADMIN));