diff --git a/docs/openapi.yaml b/docs/openapi.yaml index ef31cce..efbca50 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -82,8 +82,8 @@ paths: schema: { type: string, example: "splits,analyses" } - name: sort in: query - description: 並べ替えの基準。値の無いもの(DNF など)は最後 - schema: { type: string, enum: [date, time, moves, cross, f2l, oll, pll], default: date } + description: 並べ替えの基準。tps は段階の手数の合計を実測の時間(+2 を含めない)で割った値。値の無いもの(DNF など)は最後 + schema: { type: string, enum: [date, time, moves, tps, cross, f2l, oll, pll], default: date } - { name: order, in: query, description: asc で昇順(既定は降順), schema: { type: string, enum: [asc, desc] } } responses: "200": diff --git a/internal/solves/solves.go b/internal/solves/solves.go index ef05273..31e36a3 100644 --- a/internal/solves/solves.go +++ b/internal/solves/solves.go @@ -349,6 +349,8 @@ var SortKeys = map[string]string{ "date": "solved_at", "time": "CASE penalty WHEN 'DNF' THEN NULL WHEN '+2' THEN total_ms + 2000 ELSE total_ms END", "moves": "(SELECT SUM(move_count) FROM solve_splits WHERE solve_id = solves.id)", + // 1秒あたりの手数(段階の手数の合計 ÷ 実測の時間。+2 は含めない) + "tps": "(SELECT SUM(move_count) * 1000.0 / NULLIF(solves.total_ms, 0) FROM solve_splits WHERE solve_id = solves.id)", "cross": splitSum("'cross'"), "f2l": splitSum("'f2l1', 'f2l2', 'f2l3', 'f2l4'"), "oll": splitSum("'oll'"), diff --git a/internal/solves/solves_test.go b/internal/solves/solves_test.go index a503334..8884144 100644 --- a/internal/solves/solves_test.go +++ b/internal/solves/solves_test.go @@ -194,6 +194,7 @@ func TestListSort(t *testing.T) { {Filter{Sort: "time"}, []int64{4, 2, 1, 3}}, // 降順でも DNF は最後 {Filter{Sort: "moves", Ascending: true}, []int64{2, 1, 4, 3}}, // 段階の無いソルブは最後(新しい順) {Filter{Sort: "oll", Ascending: true}, []int64{1, 2, 4, 3}}, + {Filter{Sort: "tps"}, []int64{1, 2, 4, 3}}, // 38手/10秒 > 16手/9秒(+2 は含めない)、段階の無いソルブは最後 {Filter{Sort: "time", Ascending: true, Limit: 2, Offset: 1}, []int64{2, 4}}, } { if got := ids(tc.f); !slices.Equal(got, tc.want) { diff --git a/web/src/App.tsx b/web/src/App.tsx index b93027b..2514c0d 100644 --- a/web/src/App.tsx +++ b/web/src/App.tsx @@ -14,7 +14,10 @@ import { TimerPage } from './pages/TimerPage' import { SettingsProvider } from './settings/SettingsProvider' import { setNavCollapsed, useNavCollapsed } from './ui/prefs' -/** 左のメニュー。match に当てはまるパスでも選択中にする(解析は統計・履歴・解析の3つのタブをまとめる)。 */ +/** + * 左のメニュー。match に当てはまるパスでも選択中にする(解析は履歴・統計・AI解析の3つのタブをまとめ、履歴から開く)。 + * 中継はヘッダーの LIVE から開く。 + */ const NAV: { to: string; label: string; icon: ReactNode; end?: boolean; match?: RegExp }[] = [ { to: '/', @@ -27,6 +30,12 @@ const NAV: { to: string; label: string; icon: ReactNode; end?: boolean; match?: ), }, + { + to: '/history', + label: '解析', + match: /^\/(stats|history|analyses)(\/|$)/, + icon: , + }, { to: '/practice', label: '練習', @@ -38,22 +47,6 @@ const NAV: { to: string; label: string; icon: ReactNode; end?: boolean; match?: ), }, - { - to: '/live', - label: '中継', - icon: ( - <> - - - - ), - }, - { - to: '/stats', - label: '解析', - match: /^\/(stats|history|analyses)(\/|$)/, - icon: , - }, ] /** 左のメニューを重ねて開く(畳めない)画面幅。CSS の @media と合わせる。 */ @@ -118,6 +111,15 @@ export default function App() { Cubalyst
+ `live-link ${isActive ? 'active' : ''}`} + title="中継:キューブをつないだブラウザのタイマー画面を映す" + aria-label="中継" + > +
diff --git a/web/src/api.ts b/web/src/api.ts index baa7ca5..e1695ff 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -55,7 +55,7 @@ export type Solve = { export type NewSolve = Omit /** 一覧の並べ替えの基準。time は +2 込み、DNF と値の無いソルブ(段階が無いなど)はいつも最後。 */ -export type SolveSort = 'date' | 'time' | 'moves' | 'cross' | 'f2l' | 'oll' | 'pll' +export type SolveSort = 'date' | 'time' | 'moves' | 'tps' | 'cross' | 'f2l' | 'oll' | 'pll' export type SolveFilter = { mode?: SolveMode diff --git a/web/src/components/AnalysisTabs.tsx b/web/src/components/AnalysisTabs.tsx index d4958de..8134d40 100644 --- a/web/src/components/AnalysisTabs.tsx +++ b/web/src/components/AnalysisTabs.tsx @@ -1,12 +1,12 @@ import { NavLink } from 'react-router' const TABS = [ - { to: '/stats', label: '統計' }, { to: '/history', label: '履歴' }, + { to: '/stats', label: '統計' }, { to: '/analyses', label: 'AI解析' }, ] -/** 「解析」の中のタブ(統計・履歴・AI解析)。 */ +/** 「解析」の中のタブ(履歴・統計・AI解析)。 */ export function AnalysisTabs() { return (