mirror of
https://github.com/ksyasuda/SubMiner.git
synced 2026-03-20 12:11:28 -07:00
feat(stats): add media-detail navigation from Sessions rows; fix(tokenizer): exclude そうだ auxiliary-stem from annotations
- Added hover-revealed ↗ button on SessionRow that navigates to the
anime media-detail view for the session's videoId
- Added `sessions` origin type to MediaDetailOrigin and
openSessionsMediaDetail() / closeMediaDetail() handling so the
back button returns correctly to the Sessions tab ("Back to Sessions")
- Wired onNavigateToMediaDetail down through SessionsTab → SessionRow
- Excluded tokens with MeCab POS3 = 助動詞語幹 (e.g. そうだ grammar tails)
from subtitle annotation metadata so frequency, JLPT, and N+1 styling
no longer apply to grammar-tail tokens
- Added annotation-stage unit test and end-to-end tokenizeSubtitle test
for the そうだ exclusion path
- Updated docs-site changelog, immersion-tracking, and
subtitle-annotations pages to reflect both changes
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,8 @@
|
|||||||
- Improved stats accuracy and scale handling with Yomitan token counts, full session timelines, known-word timeline fixes, cross-media vocabulary fixes, and clearer session charts.
|
- Improved stats accuracy and scale handling with Yomitan token counts, full session timelines, known-word timeline fixes, cross-media vocabulary fixes, and clearer session charts.
|
||||||
- Improved overlay/runtime stability with quieter macOS fullscreen recovery, reduced repeated loading OSD popups, and better frequency/noise handling for subtitle annotations.
|
- Improved overlay/runtime stability with quieter macOS fullscreen recovery, reduced repeated loading OSD popups, and better frequency/noise handling for subtitle annotations.
|
||||||
- Added launcher mpv-args passthrough plus Linux plugin wrapper-name fallback for packaged installs.
|
- Added launcher mpv-args passthrough plus Linux plugin wrapper-name fallback for packaged installs.
|
||||||
|
- Added a hover-revealed ↗ button on Sessions tab rows to navigate directly to the anime media-detail view, with correct "Back to Sessions" back-navigation.
|
||||||
|
- Excluded auxiliary-stem `そうだ` grammar tails (MeCab POS3 `助動詞語幹`) from subtitle annotation metadata so frequency, JLPT, and N+1 styling no longer bleed onto grammar-tail tokens.
|
||||||
|
|
||||||
## v0.6.5 (2026-03-15)
|
## v0.6.5 (2026-03-15)
|
||||||
- Seeded the AUR checkout with the repo `.SRCINFO` template before rewriting metadata so tagged releases do not depend on prior AUR state.
|
- Seeded the AUR checkout with the repo `.SRCINFO` template before rewriting metadata so tagged releases do not depend on prior AUR state.
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ Watch time, sessions, words seen, and per-anime progress/pattern charts with con
|
|||||||
|
|
||||||
#### Sessions
|
#### Sessions
|
||||||
|
|
||||||
Expandable session history with new-word activity, cumulative totals, and pause/seek/card markers.
|
Expandable session history with new-word activity, cumulative totals, and pause/seek/card markers. Each session row exposes a hover-revealed ↗ button that navigates to the anime media-detail view for that session; pressing the back button there returns to the Sessions tab.
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ SubMiner annotates subtitle tokens in real time as they appear in the overlay. F
|
|||||||
|
|
||||||
All four are opt-in and configured under `subtitleStyle`, `ankiConnect.knownWords`, and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination.
|
All four are opt-in and configured under `subtitleStyle`, `ankiConnect.knownWords`, and `ankiConnect.nPlusOne` in your config. They apply independently — you can enable any combination.
|
||||||
|
|
||||||
Before any of those layers render, SubMiner strips annotation metadata from tokens that are usually just subtitle glue or annotation noise. Standalone particles, auxiliaries, adnominals, common explanatory endings like `んです` / `のだ`, merged trailing quote-particle forms like `...って`, repeated kana interjections, and similar non-lexical helper tokens remain hoverable in the subtitle text, but they render as plain tokens without known-word, N+1, frequency, JLPT, or name-match annotation styling.
|
Before any of those layers render, SubMiner strips annotation metadata from tokens that are usually just subtitle glue or annotation noise. Standalone particles, auxiliaries, adnominals, common explanatory endings like `んです` / `のだ`, merged trailing quote-particle forms like `...って`, auxiliary-stem grammar tails like `そうだ` (MeCab POS3 `助動詞語幹`), repeated kana interjections, and similar non-lexical helper tokens remain hoverable in the subtitle text, but they render as plain tokens without known-word, N+1, frequency, JLPT, or name-match annotation styling.
|
||||||
|
|
||||||
## N+1 Word Highlighting
|
## N+1 Word Highlighting
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
navigateToSession as navigateToSessionState,
|
navigateToSession as navigateToSessionState,
|
||||||
openAnimeEpisodeDetail,
|
openAnimeEpisodeDetail,
|
||||||
openOverviewMediaDetail,
|
openOverviewMediaDetail,
|
||||||
|
openSessionsMediaDetail,
|
||||||
switchTab,
|
switchTab,
|
||||||
} from './lib/stats-navigation';
|
} from './lib/stats-navigation';
|
||||||
|
|
||||||
@@ -110,6 +111,10 @@ export function App() {
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const navigateToSessionsMediaDetail = useCallback((videoId: number) => {
|
||||||
|
setViewState((prev) => openSessionsMediaDetail(prev, videoId));
|
||||||
|
}, []);
|
||||||
|
|
||||||
const openWordDetail = useCallback((wordId: number) => {
|
const openWordDetail = useCallback((wordId: number) => {
|
||||||
setGlobalWordId(wordId);
|
setGlobalWordId(wordId);
|
||||||
}, []);
|
}, []);
|
||||||
@@ -155,7 +160,14 @@ export function App() {
|
|||||||
}
|
}
|
||||||
onBack={() => setViewState((prev) => closeMediaDetail(prev))}
|
onBack={() => setViewState((prev) => closeMediaDetail(prev))}
|
||||||
backLabel={
|
backLabel={
|
||||||
mediaDetail.origin.type === 'overview' ? 'Back to Overview' : 'Back to Library'
|
mediaDetail.origin.type === 'overview'
|
||||||
|
? 'Back to Overview'
|
||||||
|
: mediaDetail.origin.type === 'sessions'
|
||||||
|
? 'Back to Sessions'
|
||||||
|
: 'Back to Library'
|
||||||
|
}
|
||||||
|
onNavigateToAnime={
|
||||||
|
mediaDetail.origin.type === 'anime' ? undefined : navigateToAnime
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
@@ -242,6 +254,7 @@ export function App() {
|
|||||||
onClearInitialSession={() =>
|
onClearInitialSession={() =>
|
||||||
setViewState((prev) => ({ ...prev, focusedSessionId: null }))
|
setViewState((prev) => ({ ...prev, focusedSessionId: null }))
|
||||||
}
|
}
|
||||||
|
onNavigateToMediaDetail={navigateToSessionsMediaDetail}
|
||||||
/>
|
/>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ interface SessionRowProps {
|
|||||||
onToggle: () => void;
|
onToggle: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
deleteDisabled?: boolean;
|
deleteDisabled?: boolean;
|
||||||
|
onNavigateToMediaDetail?: (videoId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
function CoverThumbnail({
|
function CoverThumbnail({
|
||||||
@@ -56,6 +57,7 @@ export function SessionRow({
|
|||||||
onToggle,
|
onToggle,
|
||||||
onDelete,
|
onDelete,
|
||||||
deleteDisabled = false,
|
deleteDisabled = false,
|
||||||
|
onNavigateToMediaDetail,
|
||||||
}: SessionRowProps) {
|
}: SessionRowProps) {
|
||||||
const displayWordCount = getSessionDisplayWordCount(session);
|
const displayWordCount = getSessionDisplayWordCount(session);
|
||||||
const knownWordsSeen = session.knownWordsSeen;
|
const knownWordsSeen = session.knownWordsSeen;
|
||||||
@@ -109,6 +111,20 @@ export function SessionRow({
|
|||||||
{'\u25B8'}
|
{'\u25B8'}
|
||||||
</div>
|
</div>
|
||||||
</button>
|
</button>
|
||||||
|
{onNavigateToMediaDetail != null && session.videoId != null ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onNavigateToMediaDetail(session.videoId!);
|
||||||
|
}}
|
||||||
|
aria-label={`View overview for ${session.canonicalTitle ?? 'Unknown Media'}`}
|
||||||
|
className="absolute right-10 top-1/2 -translate-y-1/2 w-5 h-5 rounded border border-ctp-surface2 text-transparent hover:border-ctp-blue/50 hover:text-ctp-blue hover:bg-ctp-blue/10 transition-colors opacity-0 group-hover:opacity-100 focus:opacity-100 flex items-center justify-center"
|
||||||
|
title="View anime overview"
|
||||||
|
>
|
||||||
|
{'\u2197'}
|
||||||
|
</button>
|
||||||
|
) : null}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={onDelete}
|
onClick={onDelete}
|
||||||
|
|||||||
@@ -26,9 +26,14 @@ function groupSessionsByDay(sessions: SessionSummary[]): Map<string, SessionSumm
|
|||||||
interface SessionsTabProps {
|
interface SessionsTabProps {
|
||||||
initialSessionId?: number | null;
|
initialSessionId?: number | null;
|
||||||
onClearInitialSession?: () => void;
|
onClearInitialSession?: () => void;
|
||||||
|
onNavigateToMediaDetail?: (videoId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SessionsTab({ initialSessionId, onClearInitialSession }: SessionsTabProps = {}) {
|
export function SessionsTab({
|
||||||
|
initialSessionId,
|
||||||
|
onClearInitialSession,
|
||||||
|
onNavigateToMediaDetail,
|
||||||
|
}: SessionsTabProps = {}) {
|
||||||
const { sessions, loading, error } = useSessions();
|
const { sessions, loading, error } = useSessions();
|
||||||
const [expandedId, setExpandedId] = useState<number | null>(null);
|
const [expandedId, setExpandedId] = useState<number | null>(null);
|
||||||
const [search, setSearch] = useState('');
|
const [search, setSearch] = useState('');
|
||||||
@@ -125,6 +130,7 @@ export function SessionsTab({ initialSessionId, onClearInitialSession }: Session
|
|||||||
onToggle={() => setExpandedId(expandedId === s.sessionId ? null : s.sessionId)}
|
onToggle={() => setExpandedId(expandedId === s.sessionId ? null : s.sessionId)}
|
||||||
onDelete={() => void handleDeleteSession(s)}
|
onDelete={() => void handleDeleteSession(s)}
|
||||||
deleteDisabled={deletingSessionId === s.sessionId}
|
deleteDisabled={deletingSessionId === s.sessionId}
|
||||||
|
onNavigateToMediaDetail={onNavigateToMediaDetail}
|
||||||
/>
|
/>
|
||||||
{expandedId === s.sessionId && (
|
{expandedId === s.sessionId && (
|
||||||
<div id={detailsId}>
|
<div id={detailsId}>
|
||||||
|
|||||||
@@ -1,7 +1,10 @@
|
|||||||
import type { SessionSummary } from '../types/stats';
|
import type { SessionSummary } from '../types/stats';
|
||||||
import type { TabId } from '../components/layout/TabBar';
|
import type { TabId } from '../components/layout/TabBar';
|
||||||
|
|
||||||
export type MediaDetailOrigin = { type: 'anime'; animeId: number } | { type: 'overview' };
|
export type MediaDetailOrigin =
|
||||||
|
| { type: 'anime'; animeId: number }
|
||||||
|
| { type: 'overview' }
|
||||||
|
| { type: 'sessions' };
|
||||||
|
|
||||||
export interface MediaDetailState {
|
export interface MediaDetailState {
|
||||||
videoId: number;
|
videoId: number;
|
||||||
@@ -92,6 +95,24 @@ export function openOverviewMediaDetail(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function openSessionsMediaDetail(
|
||||||
|
state: StatsViewState,
|
||||||
|
videoId: number,
|
||||||
|
): StatsViewState {
|
||||||
|
return {
|
||||||
|
activeTab: 'sessions',
|
||||||
|
selectedAnimeId: null,
|
||||||
|
focusedSessionId: null,
|
||||||
|
mediaDetail: {
|
||||||
|
videoId,
|
||||||
|
initialSessionId: null,
|
||||||
|
origin: {
|
||||||
|
type: 'sessions',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function closeMediaDetail(state: StatsViewState): StatsViewState {
|
export function closeMediaDetail(state: StatsViewState): StatsViewState {
|
||||||
if (!state.mediaDetail) {
|
if (!state.mediaDetail) {
|
||||||
return state;
|
return state;
|
||||||
@@ -106,6 +127,15 @@ export function closeMediaDetail(state: StatsViewState): StatsViewState {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (state.mediaDetail.origin.type === 'sessions') {
|
||||||
|
return {
|
||||||
|
activeTab: 'sessions',
|
||||||
|
selectedAnimeId: null,
|
||||||
|
focusedSessionId: null,
|
||||||
|
mediaDetail: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
activeTab: 'anime',
|
activeTab: 'anime',
|
||||||
selectedAnimeId: state.mediaDetail.origin.animeId,
|
selectedAnimeId: state.mediaDetail.origin.animeId,
|
||||||
|
|||||||
Reference in New Issue
Block a user