Compare commits
3 Commits
e2c1345d40
...
09800c5b17
| Author | SHA1 | Date | |
|---|---|---|---|
| 09800c5b17 | |||
| a3c7d197b1 | |||
| 01b943185f |
@@ -70,9 +70,9 @@ enum QuizGenerator {
|
|||||||
private static func blankWord(_ word: String) -> String {
|
private static func blankWord(_ word: String) -> String {
|
||||||
let chars = Array(word)
|
let chars = Array(word)
|
||||||
let letterPositions = chars.indices.filter { chars[$0].isLetter }
|
let letterPositions = chars.indices.filter { chars[$0].isLetter }
|
||||||
guard letterPositions.count > 2 else { return String(repeating: "_", count: chars.count) }
|
guard !letterPositions.isEmpty else { return word }
|
||||||
let blanks = max(1, Int(Double(letterPositions.count) * 0.4))
|
let blankCount = min(letterPositions.count - 1, Int(ceil(Double(letterPositions.count) * 0.4)))
|
||||||
let indices = Set(letterPositions.shuffled().prefix(blanks))
|
let indices = Set(letterPositions.shuffled().prefix(blankCount))
|
||||||
return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined()
|
return chars.enumerated().map { indices.contains($0.offset) ? "_" : String($0.element) }.joined()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,9 +21,11 @@ final class QuizViewController: UIViewController {
|
|||||||
private let spellingHintLabel = UILabel()
|
private let spellingHintLabel = UILabel()
|
||||||
private let resultLabel = UILabel()
|
private let resultLabel = UILabel()
|
||||||
private let resultPhoneticLabel = UILabel()
|
private let resultPhoneticLabel = UILabel()
|
||||||
|
private let countdownLabel = UILabel()
|
||||||
private let nextButton = UIButton(type: .system)
|
private let nextButton = UIButton(type: .system)
|
||||||
private var optionButtons: [UIButton] = []
|
private var optionButtons: [UIButton] = []
|
||||||
private var optionIcons: [UIImageView] = []
|
private var optionIcons: [UIImageView] = []
|
||||||
|
private var countdownTimer: Timer?
|
||||||
|
|
||||||
private let promptStack = UIStackView()
|
private let promptStack = UIStackView()
|
||||||
private let inputStack = UIStackView()
|
private let inputStack = UIStackView()
|
||||||
@@ -48,6 +50,10 @@ final class QuizViewController: UIViewController {
|
|||||||
@available(*, unavailable)
|
@available(*, unavailable)
|
||||||
required init?(coder: NSCoder) { fatalError() }
|
required init?(coder: NSCoder) { fatalError() }
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
countdownTimer?.invalidate()
|
||||||
|
}
|
||||||
|
|
||||||
override func viewDidLoad() {
|
override func viewDidLoad() {
|
||||||
super.viewDidLoad()
|
super.viewDidLoad()
|
||||||
view.backgroundColor = .systemBackground
|
view.backgroundColor = .systemBackground
|
||||||
@@ -135,7 +141,11 @@ final class QuizViewController: UIViewController {
|
|||||||
|
|
||||||
resultStack.axis = .vertical
|
resultStack.axis = .vertical
|
||||||
resultStack.spacing = 12
|
resultStack.spacing = 12
|
||||||
[resultLabel, resultPhoneticLabel, nextButton].forEach { resultStack.addArrangedSubview($0) }
|
countdownLabel.font = .preferredFont(forTextStyle: .caption1)
|
||||||
|
countdownLabel.textColor = .tertiaryLabel
|
||||||
|
countdownLabel.textAlignment = .center
|
||||||
|
countdownLabel.isHidden = true
|
||||||
|
[resultLabel, resultPhoneticLabel, countdownLabel, nextButton].forEach { resultStack.addArrangedSubview($0) }
|
||||||
|
|
||||||
let mainStack = UIStackView(arrangedSubviews: [headerStack, promptStack, inputStack, resultStack, UIView()])
|
let mainStack = UIStackView(arrangedSubviews: [headerStack, promptStack, inputStack, resultStack, UIView()])
|
||||||
mainStack.axis = .vertical
|
mainStack.axis = .vertical
|
||||||
@@ -358,6 +368,18 @@ final class QuizViewController: UIViewController {
|
|||||||
if config.isStudyMode {
|
if config.isStudyMode {
|
||||||
nextButton.isHidden = true
|
nextButton.isHidden = true
|
||||||
let delay: TimeInterval = correct ? 1.0 : 3.0
|
let delay: TimeInterval = correct ? 1.0 : 3.0
|
||||||
|
countdownLabel.text = "\(Int(delay)) 秒后继续"
|
||||||
|
countdownLabel.isHidden = false
|
||||||
|
countdownTimer?.invalidate()
|
||||||
|
var remaining = Int(delay)
|
||||||
|
countdownTimer = Timer.scheduledTimer(withTimeInterval: 1, repeats: true) { [weak self] timer in
|
||||||
|
remaining -= 1
|
||||||
|
if remaining > 0 {
|
||||||
|
self?.countdownLabel.text = "\(remaining) 秒后继续"
|
||||||
|
} else {
|
||||||
|
timer.invalidate()
|
||||||
|
}
|
||||||
|
}
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
DispatchQueue.main.asyncAfter(deadline: .now() + delay) { [weak self] in
|
||||||
self?.restart()
|
self?.restart()
|
||||||
}
|
}
|
||||||
@@ -440,6 +462,8 @@ final class QuizViewController: UIViewController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func restart() {
|
private func restart() {
|
||||||
|
countdownTimer?.invalidate()
|
||||||
|
countdownLabel.isHidden = true
|
||||||
view.subviews.compactMap { $0 as? QuizResultView }.forEach { $0.removeFromSuperview() }
|
view.subviews.compactMap { $0 as? QuizResultView }.forEach { $0.removeFromSuperview() }
|
||||||
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
questions = QuizGenerator.makeQuestions(mode: config.mode, words: config.words)
|
||||||
currentIndex = 0
|
currentIndex = 0
|
||||||
|
|||||||
@@ -5,10 +5,12 @@ final class WordCardCell: UITableViewCell {
|
|||||||
private let wordLabel = UILabel()
|
private let wordLabel = UILabel()
|
||||||
private let phoneticLabel = UILabel()
|
private let phoneticLabel = UILabel()
|
||||||
private let meaningLabel = UILabel()
|
private let meaningLabel = UILabel()
|
||||||
|
private let learnButton = UIButton(type: .system)
|
||||||
private let speakerButton = UIButton(type: .system)
|
private let speakerButton = UIButton(type: .system)
|
||||||
private var word: Word?
|
private var word: Word?
|
||||||
|
|
||||||
var onSpeak: ((Word) -> Void)?
|
var onSpeak: ((Word) -> Void)?
|
||||||
|
var onLearn: ((Word) -> Void)?
|
||||||
|
|
||||||
var zoomSourceView: UIView { cardView }
|
var zoomSourceView: UIView { cardView }
|
||||||
|
|
||||||
@@ -39,6 +41,15 @@ final class WordCardCell: UITableViewCell {
|
|||||||
textStack.axis = .vertical
|
textStack.axis = .vertical
|
||||||
textStack.spacing = 8
|
textStack.spacing = 8
|
||||||
|
|
||||||
|
learnButton.setImage(UIImage(systemName: "book",
|
||||||
|
withConfiguration: UIImage.SymbolConfiguration(pointSize: 15, weight: .medium)),
|
||||||
|
for: .normal)
|
||||||
|
learnButton.tintColor = Theme.Color.accent
|
||||||
|
learnButton.addAction(UIAction { [weak self] _ in
|
||||||
|
guard let self, let word else { return }
|
||||||
|
onLearn?(word)
|
||||||
|
}, for: .touchUpInside)
|
||||||
|
|
||||||
speakerButton.tintColor = Theme.Color.accent
|
speakerButton.tintColor = Theme.Color.accent
|
||||||
speakerButton.addAction(UIAction { [weak self] _ in
|
speakerButton.addAction(UIAction { [weak self] _ in
|
||||||
guard let self, let word else { return }
|
guard let self, let word else { return }
|
||||||
@@ -46,7 +57,7 @@ final class WordCardCell: UITableViewCell {
|
|||||||
}, for: .touchUpInside)
|
}, for: .touchUpInside)
|
||||||
speakerButton.setContentHuggingPriority(.required, for: .horizontal)
|
speakerButton.setContentHuggingPriority(.required, for: .horizontal)
|
||||||
|
|
||||||
let row = UIStackView(arrangedSubviews: [textStack, speakerButton])
|
let row = UIStackView(arrangedSubviews: [textStack, learnButton, speakerButton])
|
||||||
row.axis = .horizontal
|
row.axis = .horizontal
|
||||||
row.alignment = .center
|
row.alignment = .center
|
||||||
row.spacing = 8
|
row.spacing = 8
|
||||||
@@ -57,6 +68,8 @@ final class WordCardCell: UITableViewCell {
|
|||||||
row.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14),
|
row.leadingAnchor.constraint(equalTo: cardView.leadingAnchor, constant: 14),
|
||||||
row.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
|
row.trailingAnchor.constraint(equalTo: cardView.trailingAnchor, constant: -8),
|
||||||
row.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -10),
|
row.bottomAnchor.constraint(equalTo: cardView.bottomAnchor, constant: -10),
|
||||||
|
learnButton.widthAnchor.constraint(equalToConstant: 44),
|
||||||
|
learnButton.heightAnchor.constraint(equalToConstant: 44),
|
||||||
speakerButton.widthAnchor.constraint(equalToConstant: 44),
|
speakerButton.widthAnchor.constraint(equalToConstant: 44),
|
||||||
speakerButton.heightAnchor.constraint(equalToConstant: 44)
|
speakerButton.heightAnchor.constraint(equalToConstant: 44)
|
||||||
])
|
])
|
||||||
|
|||||||
@@ -148,6 +148,11 @@ extension WordListViewController: UITableViewDataSource, UITableViewDelegate {
|
|||||||
cell.onSpeak = { word in
|
cell.onSpeak = { word in
|
||||||
SpeechService.shared.speak(word.word)
|
SpeechService.shared.speak(word.word)
|
||||||
}
|
}
|
||||||
|
cell.onLearn = { [weak self] word in
|
||||||
|
guard let self else { return }
|
||||||
|
let config = QuizConfig(mode: .spelling, words: [word], isStudyMode: true)
|
||||||
|
navigationController?.pushViewController(QuizViewController(config: config), animated: true)
|
||||||
|
}
|
||||||
return cell
|
return cell
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user