Skip to content
Open
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
15 changes: 15 additions & 0 deletions ios/AnswerTextField.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,19 @@ class AnswerTextField: UITextField {
}
return super.textInputMode
}

// Add a property to toggle interaction. Doing this will allow the textfield to process events,
// which is impossible when simply set to disabled.
var isInteractive: Bool = true {
didSet {
// Dismiss the keyboard if the field is no longer interactive
if !isInteractive && isFirstResponder {
resignFirstResponder()
}
}
}

override func becomeFirstResponder() -> Bool {
isInteractive ? super.becomeFirstResponder() : false
}
}
16 changes: 16 additions & 0 deletions ios/ReviewSettingsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,18 @@ class ReviewSettingsViewController: UITableViewController, TKMViewController {
ankiModeCombineReadingMeaningIndexPath = model.add(ankiModeCombineReadingMeaning,
hidden:!Settings.ankiMode)

let ankiModeSwipeGestures = SwitchModelItem(style: .subtitle,
title: "Enable swipe gestures",
subtitle: "Use swipes for marking reviews with Anki mode enabled",
on: Settings
.ankiModeEnableSwipeGestures) { [
unowned self
] in
ankiModeSwipeGesturesSwitchChanged($0)
}
ankiModeCombineReadingMeaningIndexPath = model.add(ankiModeSwipeGestures,
hidden:!Settings.ankiMode)

model.add(section: "Audio")
model.add(SwitchModelItem(style: .subtitle,
title: "Play audio automatically",
Expand Down Expand Up @@ -354,6 +366,10 @@ class ReviewSettingsViewController: UITableViewController, TKMViewController {
Settings.ankiModeCombineReadingMeaning = switchView.isOn
}

private func ankiModeSwipeGesturesSwitchChanged(_ switchView: UISwitch) {
Settings.ankiModeEnableSwipeGestures = switchView.isOn
}

private func playAudioAutomaticallySwitchChanged(_ switchView: UISwitch) {
Settings.playAudioAutomatically = switchView.isOn
}
Expand Down
146 changes: 110 additions & 36 deletions ios/ReviewViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -159,7 +159,8 @@ protocol ReviewViewControllerDelegate: AnyObject {
menuButton: UIButton)
}

class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelegate {
class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelegate,
SwipeableContainerDelegate {
private var kanaInput: TKMKanaInput!
private let hapticGenerator = UIImpactFeedbackGenerator(style: UIImpactFeedbackGenerator
.FeedbackStyle.light)
Expand All @@ -185,6 +186,8 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega

private var isPracticeSession = false

private var swipeContainer: SwipeableContainer!

// These are set to match the keyboard animation.
private var animationDuration: Double = kDefaultAnimationDuration
private var animationCurve: UIView.AnimationCurve = kDefaultAnimationCurve
Expand Down Expand Up @@ -302,21 +305,40 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega

questionLabel.isUserInteractionEnabled = false

let shortPressRecognizer =
UITapGestureRecognizer(target: self, action: #selector(didShortPressQuestionLabel))
questionBackground.addGestureRecognizer(shortPressRecognizer)
// set up swipe gestures for answering
swipeContainer = SwipeableContainer()
swipeContainer.delegate = self
swipeContainer.translatesAutoresizingMaskIntoConstraints = false
view.insertSubview(swipeContainer, belowSubview: promptBackground)

// Add constraints to match question background
NSLayoutConstraint.activate([
swipeContainer.leadingAnchor.constraint(equalTo: questionBackground.leadingAnchor),
swipeContainer.trailingAnchor.constraint(equalTo: questionBackground.trailingAnchor),
swipeContainer.topAnchor.constraint(equalTo: questionBackground.topAnchor),
swipeContainer.bottomAnchor.constraint(equalTo: questionBackground.bottomAnchor),
])

// font cycling tap recognizers
func setupTapQuestionRecognizer(numberOfTapsRequired: Int = 1) -> UITapGestureRecognizer {
let tapRecognizer = UITapGestureRecognizer(target: self,
action: #selector(didTapQuestionView))
swipeContainer.addGestureRecognizer(tapRecognizer)
tapRecognizer.numberOfTapsRequired = numberOfTapsRequired
return tapRecognizer
}

let singleTapRecognizer = setupTapQuestionRecognizer()
let doubleTapRecognizer = setupTapQuestionRecognizer(numberOfTapsRequired: 2)
let tripleTapRecognizer = setupTapQuestionRecognizer(numberOfTapsRequired: 3)

let leftSwipeRecognizer = UISwipeGestureRecognizer(target: self,
action: #selector(didSwipeQuestionLabel))
leftSwipeRecognizer.direction = .left
questionBackground.addGestureRecognizer(leftSwipeRecognizer)
let rightSwipeRecognizer = UISwipeGestureRecognizer(target: self,
action: #selector(didSwipeQuestionLabel))
rightSwipeRecognizer.direction = .right
questionBackground.addGestureRecognizer(rightSwipeRecognizer)
// make sure to fail the tap gesture recognizers with less taps
singleTapRecognizer.require(toFail: doubleTapRecognizer)
doubleTapRecognizer.require(toFail: tripleTapRecognizer)

leftSwipeRecognizer.require(toFail: shortPressRecognizer)
rightSwipeRecognizer.require(toFail: shortPressRecognizer)
// add a tap gesture for the answer bar for submitting in anki mode
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(didTapAnswerBar))
answerField.addGestureRecognizer(tapRecognizer)

resizeViewsForFontSize()
viewDidLayoutSubviews()
Expand Down Expand Up @@ -728,6 +750,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
submitButton.isHidden = false
}
}
updateSwipeConfiguration()

// Change the submit button icon.
let submitButtonImage = shown ? forwardArrowImage :
Expand Down Expand Up @@ -760,7 +783,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
// Enable/disable the answer field, and set its first responder status.
// This makes the keyboard appear or disappear immediately. We need this animation to happen
// here so it's in sync with the others.
answerField.isEnabled = !shown && !Settings.ankiMode
answerField.isInteractive = !shown && !Settings.ankiMode
if updateFirstResponder {
if !shown {
answerField.becomeFirstResponder()
Expand Down Expand Up @@ -814,6 +837,8 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
}
}

updateSwipeConfiguration()

// This makes sure taps are still processed and not ignored, even when the closing animation
// after a button press was not completed
if Settings.ankiMode, ankiModeCachedSubmit { submit() }
Expand Down Expand Up @@ -896,19 +921,12 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
questionLabel.font = UIFont(name: fontName, size: questionLabelFontSize())
}

@objc func didShortPressQuestionLabel(_: UITapGestureRecognizer) {
toggleFont()
if Settings.ankiMode {
if !isAnimatingSubjectDetailsView { submit() }
else { ankiModeCachedSubmit = true }
}
}

@objc func didSwipeQuestionLabel(_ sender: UISwipeGestureRecognizer) {
if sender.direction == .left {
showNextCustomFont()
} else if sender.direction == .right {
showPreviousCustomFont()
@objc func didTapQuestionView(_ sender: UITapGestureRecognizer) {
switch sender.numberOfTapsRequired {
case 1: toggleFont()
case 2: showNextCustomFont()
case 3: showPreviousCustomFont()
default: break
}
}

Expand Down Expand Up @@ -944,6 +962,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
}

func quickSettingsChanged() {
updateSwipeConfiguration()
if subjectDetailsView.isHidden {
updateViewForCurrentTask(updateFirstResponder: false)
}
Expand Down Expand Up @@ -1006,7 +1025,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
markAnswer(.AskAgainLater)
return
}
if !answerField.isEnabled, !Settings.ankiMode {
if !answerField.isInteractive, !Settings.ankiMode {
if !subjectDetailsView.isHidden {
subjectDetailsView.saveStudyMaterials()
}
Expand All @@ -1020,7 +1039,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
@objc func backspaceKeyPressed() {
answerField.text = nil
answerField.textColor = TKMStyle.Color.label
answerField.isEnabled = true
answerField.isInteractive = true
answerField.becomeFirstResponder()
}

Expand Down Expand Up @@ -1144,7 +1163,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
UIView.animate(withDuration: animationDuration,
animations: {
self.answerField.textColor = .systemRed
self.answerField.isEnabled = false
self.answerField.isInteractive = false
self.revealAnswerButton.alpha = 1.0
self.submitButton.setImage(self.forwardArrowImage, for: .normal)
})
Expand All @@ -1170,6 +1189,13 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
updateFirstResponder: true)
}

@objc func didTapAnswerBar(_: UITapGestureRecognizer) {
if Settings.ankiMode {
if !isAnimatingSubjectDetailsView { submit() }
else { ankiModeCachedSubmit = true }
}
}

// MARK: - Ignoring incorrect answers

@IBAction func addSynonymButtonPressed(_: Any) {
Expand All @@ -1183,7 +1209,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega

c.addAction(UIAlertAction(title: "My answer was correct",
style: .default,
handler: { _ in self.markCorrect() }))
handler: { _ in self.markOverrideCorrect() }))
if Settings.ankiMode {
c.addAction(UIAlertAction(title: "My answer was incorrect",
style: .default,
Expand All @@ -1203,7 +1229,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
present(c, animated: true, completion: nil)
}

@objc func markCorrect() {
@objc func markOverrideCorrect() {
markAnswer(.OverrideAnswerCorrect)
}

Expand All @@ -1229,6 +1255,54 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
super.canPerformAction(action, withSender: sender)
}

// MARK: - Swipe Gesture Delegate

func containerDidSwipeRight(_: SwipeableContainer) {
// Handle correct answer
if !subjectDetailsView.isHidden {
// call the same function that is used by the synonyms menu to mark correct
markOverrideCorrect()
} else {
// use the marking function for outside the details view
markAnswer(.Correct)
}
}

func containerDidSwipeLeft(_: SwipeableContainer) {
if subjectDetailsView.isHidden {
_ = session.markAnswer(.Incorrect, isPracticeSession: isPracticeSession)
}
randomTask()
}

func containerDidSwipeDown(_: SwipeableContainer) {
// Skip question
if Settings.allowSkippingReviews {
markAnswer(.AskAgainLater)
return
}
}

func containerDidSwipeUp(_: SwipeableContainer) {
submit()
}

private func updateSwipeConfiguration() {
var config = SwipeableContainer.SwipeConfiguration()

guard Settings.ankiMode,
Settings.ankiModeEnableSwipeGestures else {
swipeContainer.updateSwipeConfiguration(config)
return
}

config.isRightEnabled = true
config.isLeftEnabled = true
config.isDownEnabled = Settings.allowSkippingReviews
config.isUpEnabled = subjectDetailsView.isHidden
swipeContainer.updateSwipeConfiguration(config)
}

// MARK: - SubjectDelegate

func didTapSubject(_ subject: TKMSubject) {
Expand All @@ -1252,7 +1326,7 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
discoverabilityTitle: "Continue")
var keyCommands: [UIKeyCommand] = []

if !answerField.isEnabled, subjectDetailsView.isHidden {
if !answerField.isInteractive, subjectDetailsView.isHidden {
// Continue when a wrong answer has been entered but the subject details view is hidden.
keyCommands.append(contentsOf: [UIKeyCommand(input: "\u{8}",
modifierFlags: [],
Expand All @@ -1276,11 +1350,11 @@ class ReviewViewController: UIViewController, UITextFieldDelegate, SubjectDelega
discoverabilityTitle: "Ask again later"),
UIKeyCommand(input: "c",
modifierFlags: [.command],
action: #selector(markCorrect),
action: #selector(markOverrideCorrect),
discoverabilityTitle: "Mark correct"),
UIKeyCommand(input: "c",
modifierFlags: [.control],
action: #selector(markCorrect)),
action: #selector(markOverrideCorrect)),
UIKeyCommand(input: "i",
modifierFlags: [.command],
action: #selector(markIncorrect),
Expand Down
2 changes: 2 additions & 0 deletions ios/Settings.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,8 @@ protocol SettingProtocol {
@Setting(false, #keyPath(ankiMode)) static var ankiMode: Bool
@Setting(false,
#keyPath(ankiModeCombineReadingMeaning)) static var ankiModeCombineReadingMeaning: Bool
@Setting(false,
#keyPath(ankiModeEnableSwipeGestures)) static var ankiModeEnableSwipeGestures: Bool
@Setting(true, #keyPath(showPreviousLevelGraph)) static var showPreviousLevelGraph: Bool
@Setting(true, #keyPath(showKanaOnlyVocab)) static var showKanaOnlyVocab: Bool

Expand Down
8 changes: 8 additions & 0 deletions ios/Style.swift
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,14 @@ class TKMStyle: NSObject {
static let explosionColor2 = UIColor(red: 230.0 / 255, green: 57.0 / 255, blue: 91.0 / 255,
alpha: 1.0)

static let incorrectAnswerColor: UIColor = .init(red: 234.0 / 255, green: 51.0 / 255,
blue: 61.0 / 255,
alpha: 1.0) // also srsLevelDownColor
static let correctAnswerColor: UIColor = .init(red: 151.0 / 255, green: 202.0 / 255,
blue: 62.0 / 255, alpha: 1.0)
static let srsLevelUpColor: UIColor = .init(red: 91.0 / 255, green: 168.0 / 255, blue: 51.0 / 255,
alpha: 1.0)

static var radicalGradient: [CGColor] { [radicalColor1.cgColor, radicalColor2.cgColor] }
static var kanjiGradient: [CGColor] { [kanjiColor1.cgColor, kanjiColor2.cgColor] }
static var vocabularyGradient: [CGColor] { [vocabularyColor1.cgColor, vocabularyColor2.cgColor] }
Expand Down
Loading