Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,25 @@ it needs to read like release notes rather than a commit log.

### Added

- Listening and dictation practice (听力 / 听写). A text with a narration opens
a practice screen that plays one sentence at a time, repeated as many times
as you ask with a gap to write in, and marks what you typed against what was
said. Sentence boundaries are found by listening to the recording once, on
this device — the audio is not sent anywhere — and aligning what was heard
against the transcript already in your library, so a recogniser's mistakes
are bridged by the words either side of them rather than becoming yours.
The transcript stays hidden until you check, which is the whole point.
- Dictation marking tells the three kinds of wrong apart. A character that
sounds exactly like the right one — 真 for 针, both `zhen1` — is marked as a
homophone, which is an input-method slip rather than a listening mistake and
does not keep the sentence in the drill. A character with the right syllable
and the wrong tone — 是 for 时 — is marked separately, because tones are what
an ear loses first. Everything else is a mishearing. No other tool this
project was measured against can make that distinction; HanReader can
because the readings are already open.
- Sentences can be graded easy, middling or hard, and a filter drills only the
ones still worth drilling: graded hard, never attempted, or last attempted
below the pass mark.
- Per-text audio. Attach a narration to any text and it plays in a transport
below the reader, with a scrubber, ten-second skips, five speeds and lock
screen and Control Centre controls. The playhead is remembered, so the next
Expand Down
9 changes: 8 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@ separate window:
it never jumps as you move from word to word.
- **Hear it.** Tapping a word speaks it.
- **Follow along with audio.** Attach a narration file to a text and scrub it at
0.75× to 1.5×. *(Arrives in M7.)*
0.75× to 1.5×.
- **Practise listening and dictation (听力 / 听写).** HanReader listens to the
narration once, on this device, to work out where each sentence is said. Then
it plays one sentence at a time — repeated as many times as you like, with a
gap to write in — and marks what you typed against what was said, character
by character. A wrong character that *sounds* the same is told apart from one
you misheard, and from one whose tone you missed.
- **Two reading modes** — visibly word-segmented, or continuous as it would
actually be printed.
- **It remembers.** Your position and the words you have revealed survive
Expand Down Expand Up @@ -107,6 +113,7 @@ framework imports. Details in [`Docs/ARCHITECTURE.md`](Docs/ARCHITECTURE.md).
- [x] **M5** — **the macOS reader: first usable version**
- [x] **M6** — **BKRS DSL import**
- [x] **M7** — **audio playback**
- [ ] **M11** — **listening and dictation practice (听力 / 听写)** — practice screen done, phrase-boundary editor to come
- [ ] **M8** — iOS and iPadOS
- [ ] **M9** — accessibility and Russian localization
- [ ] **M10** — 1.0
Expand Down
29 changes: 29 additions & 0 deletions Sources/HanReaderUI/Design/DesignTokens.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
// HanReader — MIT licensed. See LICENSE.

import HanReaderCore
import SwiftUI

/// Colour roles for the reading surface.
Expand All @@ -26,6 +27,34 @@ nonisolated enum ReaderColor {
/// Punctuation and whitespace, which are never looked up.
static let inertText = Color.primary.opacity(0.75)

/// A phrase nobody has tried yet, one they have passed, one that still
/// needs work.
static let untried = Color.secondary.opacity(0.3)
static let passed = Color.green.opacity(0.7)
static let needsWork = Color.orange.opacity(0.8)

/// How a marked character is coloured.
///
/// Three distinct colours for the three kinds of wrong, because that
/// distinction is the point of the screen: a homophone is the learner's
/// input method, a tone slip is their ear for tones, and a mishearing
/// is their ear. Flattening them to one red would throw away the only
/// thing this tells them that a plain diff could not.
static func dictation(_ outcome: DictationOutcome) -> Color {
switch outcome {
case .correct: .primary
case .homophone: .blue
case .toneSlip: .purple
case .misheard: .red
case .missing: .orange
case .extra: .secondary
}
}

static func dictationFill(_ outcome: DictationOutcome) -> Color {
outcome == .correct ? .clear : dictation(outcome).opacity(0.14)
}

/// The fill behind a highlighted token.
///
/// Takes the resolved `TokenHighlight` rather than an opacity, so that the
Expand Down
Loading
Loading