Fixes #28 — Curated reflexive verb list on detail + practice filter

Bundles the 100 most common reflexive verbs from spanishwithdaniel.com as a
canonical list and wires it through the UI. Compound list entries (recibirse
/ graduarse, equivocarse / confundirse) are split. Trailing prepositions and
set-phrase completions are captured as usageHint (e.g. acordarse "de",
ponerse "de acuerdo").

ReflexiveVerbStore loads the JSON at launch and exposes lookups by base
infinitive, both via @Environment for SwiftUI and a static shared instance
for services. Verbs whose bare infinitive isn't in the list skip the UI
treatment silently.

VerbDetailView shows a new Reflexive section with the reflexive infinitive,
usage hint, and English meaning when there is a match. VerbListView gains a
"Reflexive verbs only" filter alongside the existing Level and Irregularity
filters. Settings adds the same flag so it also constrains the practice
pool; PracticeSessionService applies the reflexive filter in all six pick
paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey t
2026-04-22 10:56:04 -05:00
parent 4093b5a7f3
commit 98badc98ad
13 changed files with 360 additions and 23 deletions

View File

@@ -63,9 +63,11 @@
8DC1CB93333F94C5297D33BF /* GrammarExerciseView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 07F24ED76059609D6857EC97 /* GrammarExerciseView.swift */; };
90B76E34F195223580F7CCCF /* DictionaryService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 76BE2A08EC694FF784ED5575 /* DictionaryService.swift */; };
943A94A8C71919F3EFC0E8FA /* UserProgress.swift in Sources */ = {isa = PBXBuildFile; fileRef = E536AD1180FE10576EAC884A /* UserProgress.swift */; };
97A2088134FC6CB41C507182 /* reflexive_verbs.json in Resources */ = {isa = PBXBuildFile; fileRef = 3644B5ED77F29A65877D926A /* reflexive_verbs.json */; };
97EFCF6724CE59DC4F0274FD /* AchievementService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1C42EA0EBD4CB1E10A82BA25 /* AchievementService.swift */; };
9D9FD3853C5C969C62AE9999 /* StartupCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = A4B95B276C054DBFE508C4D1 /* StartupCoordinator.swift */; };
9F0ACDC1F4ACB1E0D331283D /* CheckpointExamView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 34C67DD1A1CB9B8B5A2BDCED /* CheckpointExamView.swift */; };
A4DA8CF1957A4FE161830AB2 /* ReflexiveVerbStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 940826D9ED5C18D2C4E7B2C7 /* ReflexiveVerbStore.swift */; };
A7DF435F99E66E067F2B33E1 /* ListeningView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 20D1904DF07E0A6816134CF3 /* ListeningView.swift */; };
A9959AE6C87B4AD21554E401 /* FullTableView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 711CB7539EF5887F6F7B8B82 /* FullTableView.swift */; };
AAC6F85A1C3B6C1186E1656A /* TenseEndingTable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 69D98E1564C6538056D81200 /* TenseEndingTable.swift */; };
@@ -153,6 +155,7 @@
2931634BEB33B93429CE254F /* VocabFlashcardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VocabFlashcardView.swift; sourceTree = "<group>"; };
30EF2362D9FFF9B07A45CE6D /* StreakCalendarView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StreakCalendarView.swift; sourceTree = "<group>"; };
34C67DD1A1CB9B8B5A2BDCED /* CheckpointExamView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckpointExamView.swift; sourceTree = "<group>"; };
3644B5ED77F29A65877D926A /* reflexive_verbs.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = reflexive_verbs.json; sourceTree = "<group>"; };
3695075616689E72DBB26D4C /* HandwritingRecognizer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HandwritingRecognizer.swift; sourceTree = "<group>"; };
39908548430FDF01D76201FB /* TextbookChapterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TextbookChapterView.swift; sourceTree = "<group>"; };
3A96C065B8787DEC6818E497 /* ConversationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConversationService.swift; sourceTree = "<group>"; };
@@ -197,6 +200,7 @@
8C2D88FF9A3B0590B22C7837 /* conjuga_data.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = conjuga_data.json; sourceTree = "<group>"; };
8C935ECDF8A5D8D6FA541E20 /* GuideView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GuideView.swift; sourceTree = "<group>"; };
8E9BCDBB9BC24F5C8117767E /* WordOfDayWidget.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WordOfDayWidget.swift; sourceTree = "<group>"; };
940826D9ED5C18D2C4E7B2C7 /* ReflexiveVerbStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReflexiveVerbStore.swift; sourceTree = "<group>"; };
9708FF3CF33E4765DB225F93 /* ConjugaWidgetExtension.appex */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = "wrapper.app-extension"; path = ConjugaWidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
9E1FB35614B709E6B1D1D017 /* Conjuga.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Conjuga.entitlements; sourceTree = "<group>"; };
A014EEC3EE08E945FBBA5335 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@@ -257,6 +261,7 @@
1EB4830F9289AACC82D753F8 /* ConjugaApp.swift */,
BC273716CD14A99EFF8206CA /* course_data.json */,
7E6AF62A3A949630E067DC22 /* Info.plist */,
3644B5ED77F29A65877D926A /* reflexive_verbs.json */,
353C5DE41FD410FA82E3AED7 /* Models */,
1994867BC8E985795A172854 /* Services */,
BFC1AEBE02CE22E6474FFEA6 /* Utilities */,
@@ -298,6 +303,7 @@
842DB48F8570C39CDCFF2F57 /* PracticeSessionService.swift */,
4AE3D1D8723D5C41D3501774 /* PronunciationService.swift */,
777C696A841803D5B775B678 /* ReferenceStore.swift */,
940826D9ED5C18D2C4E7B2C7 /* ReflexiveVerbStore.swift */,
CBCF6FCFA6B00151C2371E77 /* ReviewStore.swift */,
49E3AD244327CBF24B7A2752 /* SpeechService.swift */,
5C0E6EAFC0D24928BA956FA5 /* SRSEngine.swift */,
@@ -606,6 +612,7 @@
F59655A8B8FCE6264315DD33 /* Assets.xcassets in Resources */,
CF9E48ADF0501FB79F3DDB7B /* conjuga_data.json in Resources */,
2B5B2D63DC9C290F66890A4A /* course_data.json in Resources */,
97A2088134FC6CB41C507182 /* reflexive_verbs.json in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
@@ -663,6 +670,7 @@
0D0D3B5CC128D1A1D1252282 /* PronunciationService.swift in Sources */,
53A0AC57EAC44B676C997374 /* QuizType.swift in Sources */,
DF82C2579F9889DDB06362CC /* ReferenceStore.swift in Sources */,
A4DA8CF1957A4FE161830AB2 /* ReflexiveVerbStore.swift in Sources */,
FC7873F97017532C215DAD34 /* ReviewCard.swift in Sources */,
728702D9AA7A8BDABBA62513 /* ReviewStore.swift in Sources */,
51D072AF30F4B12CD3E8F918 /* SRSEngine.swift in Sources */,

View File

@@ -41,6 +41,7 @@ struct ConjugaApp: App {
@State private var studyTimer = StudyTimerService()
@State private var dictionary = DictionaryService()
@State private var verbExampleCache = VerbExampleCache()
@State private var reflexiveStore = ReflexiveVerbStore()
let localContainer: ModelContainer
let cloudContainer: ModelContainer
@@ -115,6 +116,7 @@ struct ConjugaApp: App {
.environment(studyTimer)
.environment(dictionary)
.environment(verbExampleCache)
.environment(reflexiveStore)
.task {
let needsSeed = await DataLoader.needsSeeding(container: localContainer)
if needsSeed {

View File

@@ -14,6 +14,7 @@ final class UserProgress {
var selectedLevel: String = "basic"
var showVosotros: Bool = true
var autoFillStem: Bool = false
var showReflexiveVerbsOnly: Bool = false
// Legacy CloudKit array-backed fields retained for migration compatibility.
var enabledTenses: [String] = []

View File

@@ -8,8 +8,10 @@ struct PracticeSettings: Sendable {
let enabledTenses: Set<String>
let enabledIrregularCategories: Set<IrregularSpan.SpanCategory>
let showVosotros: Bool
let showReflexiveVerbsOnly: Bool
let reflexiveBaseInfinitives: Set<String>
init(progress: UserProgress?) {
init(progress: UserProgress?, reflexiveBaseInfinitives: Set<String> = []) {
let resolvedTenses = progress?.enabledTenseIDs ?? []
let resolvedLevels = progress?.selectedVerbLevels ?? []
self.selectedLevel = progress?.selectedLevel ?? VerbLevel.basic.rawValue
@@ -17,6 +19,8 @@ struct PracticeSettings: Sendable {
self.enabledTenses = Set(resolvedTenses)
self.enabledIrregularCategories = progress?.enabledIrregularCategories ?? []
self.showVosotros = progress?.showVosotros ?? true
self.showReflexiveVerbsOnly = progress?.showReflexiveVerbsOnly ?? false
self.reflexiveBaseInfinitives = reflexiveBaseInfinitives
}
var selectionTenseIDs: [String] {
@@ -41,16 +45,25 @@ struct FullTablePrompt {
struct PracticeSessionService {
let localContext: ModelContext
let cloudContext: ModelContext
let reflexiveBaseInfinitives: Set<String>
private let referenceStore: ReferenceStore
init(localContext: ModelContext, cloudContext: ModelContext) {
init(
localContext: ModelContext,
cloudContext: ModelContext,
reflexiveBaseInfinitives: Set<String> = []
) {
self.localContext = localContext
self.cloudContext = cloudContext
self.reflexiveBaseInfinitives = reflexiveBaseInfinitives
self.referenceStore = ReferenceStore(context: localContext)
}
func settings() -> PracticeSettings {
PracticeSettings(progress: ReviewStore.fetchOrCreateUserProgress(context: cloudContext))
PracticeSettings(
progress: ReviewStore.fetchOrCreateUserProgress(context: cloudContext),
reflexiveBaseInfinitives: reflexiveBaseInfinitives
)
}
func nextCard(for focusMode: FocusMode, excludingVerbId lastVerbId: Int? = nil) -> PracticeCardLoad? {
@@ -86,7 +99,10 @@ struct PracticeSessionService {
let settings = settings()
// Full Table practice is regular-only, so the irregular-category setting is
// deliberately ignored here (applying it would empty the pool).
let verbs = referenceStore.fetchVerbs(selectedLevels: settings.selectedLevels)
let verbs = applyReflexiveFilter(
to: referenceStore.fetchVerbs(selectedLevels: settings.selectedLevels),
settings: settings
)
guard !verbs.isEmpty else { return nil }
for _ in 0..<40 {
@@ -143,6 +159,27 @@ struct PracticeSessionService {
return buildCardLoad(verb: verb, form: form)
}
/// When the user has "Reflexive verbs only" enabled, restrict the allowed
/// verb-id set to IDs whose infinitive is in the curated list.
/// No-op otherwise.
private func applyReflexiveFilter(to ids: Set<Int>, settings: PracticeSettings) -> Set<Int> {
guard settings.showReflexiveVerbsOnly, !settings.reflexiveBaseInfinitives.isEmpty else {
return ids
}
let matching = ids.filter { id in
guard let verb = referenceStore.fetchVerb(id: id) else { return false }
return settings.reflexiveBaseInfinitives.contains(verb.infinitive.lowercased())
}
return matching
}
private func applyReflexiveFilter(to verbs: [Verb], settings: PracticeSettings) -> [Verb] {
guard settings.showReflexiveVerbsOnly, !settings.reflexiveBaseInfinitives.isEmpty else {
return verbs
}
return verbs.filter { settings.reflexiveBaseInfinitives.contains($0.infinitive.lowercased()) }
}
private func buildCardLoad(verb: Verb, form: VerbForm) -> PracticeCardLoad {
let spans = referenceStore.fetchSpans(
verbId: form.verbId,
@@ -164,9 +201,12 @@ struct PracticeSessionService {
private func fetchDueCard(excluding lastVerbId: Int?) -> ReviewCard? {
let settings = settings()
let allowedVerbIds = referenceStore.allowedVerbIDs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
let allowedVerbIds = applyReflexiveFilter(
to: referenceStore.allowedVerbIDs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
),
settings: settings
)
let now = Date()
var descriptor = FetchDescriptor<ReviewCard>(
@@ -194,9 +234,12 @@ struct PracticeSessionService {
private func pickWeakForm() -> VerbForm? {
let settings = settings()
let allowedVerbIds = referenceStore.allowedVerbIDs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
let allowedVerbIds = applyReflexiveFilter(
to: referenceStore.allowedVerbIDs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
),
settings: settings
)
let descriptor = FetchDescriptor<ReviewCard>(
@@ -221,9 +264,12 @@ struct PracticeSessionService {
let settings = settings()
// Focus mode explicitly selects one irregular category, so the user's
// settings-level irregular filter is deliberately skipped here.
let allowedVerbIds = referenceStore.allowedVerbIDs(
selectedLevels: settings.selectedLevels,
irregularCategories: []
let allowedVerbIds = applyReflexiveFilter(
to: referenceStore.allowedVerbIDs(
selectedLevels: settings.selectedLevels,
irregularCategories: []
),
settings: settings
)
let typeRange: ClosedRange<Int>
@@ -261,9 +307,12 @@ struct PracticeSessionService {
private func pickCommonTenseForm() -> VerbForm? {
let settings = settings()
let coreTenseIDs = TenseID.coreTenseIDs
let verbs = referenceStore.fetchVerbs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
let verbs = applyReflexiveFilter(
to: referenceStore.fetchVerbs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
),
settings: settings
)
guard let verb = verbs.randomElement() else { return nil }
@@ -277,9 +326,12 @@ struct PracticeSessionService {
private func pickRandomForm() -> VerbForm? {
let settings = settings()
let verbs = referenceStore.fetchVerbs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
let verbs = applyReflexiveFilter(
to: referenceStore.fetchVerbs(
selectedLevels: settings.selectedLevels,
irregularCategories: settings.enabledIrregularCategories
),
settings: settings
)
guard let verb = verbs.randomElement() else { return nil }

View File

@@ -0,0 +1,59 @@
import Foundation
import SharedModels
/// Loads and queries the curated reflexive-verb list bundled with the app
/// (Gitea issue #28). One JSON load at init; in-memory lookup thereafter.
///
/// `entries(for:)` returns a list because a single base infinitive may map to
/// multiple reflexive entries e.g., `ponerse` covers both "to put on
/// (clothing) / to become" and "to come to an agreement (with)".
@MainActor
@Observable
final class ReflexiveVerbStore {
/// Process-wide accessor for services that can't use @Environment injection
/// (e.g. PracticeSessionService called from ViewModels). Views should still
/// prefer @Environment(ReflexiveVerbStore.self) for consistency.
static let shared = ReflexiveVerbStore()
private(set) var entries: [ReflexiveVerb] = []
private var indexByBase: [String: [ReflexiveVerb]] = [:]
/// Set of base infinitives present in the list. Cheap lookup for filters.
private(set) var baseInfinitives: Set<String> = []
init(bundle: Bundle = .main) {
load(from: bundle)
}
/// All reflexive entries whose base infinitive matches (case-insensitive).
func entries(for baseInfinitive: String) -> [ReflexiveVerb] {
indexByBase[baseInfinitive.lowercased()] ?? []
}
/// Convenience true when the verb's bare infinitive appears in the list.
func isReflexive(baseInfinitive: String) -> Bool {
baseInfinitives.contains(baseInfinitive.lowercased())
}
private func load(from bundle: Bundle) {
guard let url = bundle.url(forResource: "reflexive_verbs", withExtension: "json"),
let data = try? Data(contentsOf: url) else {
print("[ReflexiveVerbStore] bundled reflexive_verbs.json not found")
return
}
do {
let decoded = try JSONDecoder().decode([ReflexiveVerb].self, from: data)
entries = decoded
var index: [String: [ReflexiveVerb]] = [:]
for entry in decoded {
index[entry.baseInfinitive.lowercased(), default: []].append(entry)
}
indexByBase = index
baseInfinitives = Set(index.keys)
print("[ReflexiveVerbStore] loaded \(decoded.count) entries (\(baseInfinitives.count) distinct base infinitives)")
} catch {
print("[ReflexiveVerbStore] decode failed: \(error)")
}
}
}

View File

@@ -96,7 +96,11 @@ final class PracticeViewModel {
currentSpans = []
hasCards = true
isLoading = true
let service = PracticeSessionService(localContext: localContext, cloudContext: cloudContext)
let service = PracticeSessionService(
localContext: localContext,
cloudContext: cloudContext,
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
)
guard let cardLoad = service.nextCard(for: focusMode, excludingVerbId: currentVerb?.id) else {
clearCurrentCard()
hasCards = false

View File

@@ -243,7 +243,11 @@ struct FullTableView: View {
results = Array(repeating: nil, count: 6)
correctForms = []
drawings = Array(repeating: PKDrawing(), count: 6)
let service = PracticeSessionService(localContext: modelContext, cloudContext: cloudModelContext)
let service = PracticeSessionService(
localContext: modelContext,
cloudContext: cloudModelContext,
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
)
guard let prompt = service.randomFullTablePrompt() else {
currentVerb = nil
currentTense = nil
@@ -312,7 +316,11 @@ struct FullTableView: View {
if allCorrect { sessionCorrect += 1 }
if let verb = currentVerb, let tense = currentTense {
let service = PracticeSessionService(localContext: modelContext, cloudContext: cloudModelContext)
let service = PracticeSessionService(
localContext: modelContext,
cloudContext: cloudModelContext,
reflexiveBaseInfinitives: ReflexiveVerbStore.shared.baseInfinitives
)
let reviewResults = Dictionary(uniqueKeysWithValues: personsToShow.map { ($0.index, results[$0.index] == true) })
_ = service.recordFullTableReview(verbId: verb.id, tenseId: tense.id, results: reviewResults)
}

View File

@@ -97,6 +97,20 @@ struct SettingsView: View {
Text("Leave all off to include regular and irregular verbs. Enable any to restrict practice to those irregularity types.")
}
Section {
Toggle("Reflexive verbs only", isOn: Binding(
get: { progress?.showReflexiveVerbsOnly ?? false },
set: { enabled in
progress?.showReflexiveVerbsOnly = enabled
saveProgress()
}
))
} header: {
Text("Reflexive")
} footer: {
Text("When on, practice pulls only from the curated list of common reflexive verbs.")
}
Section("Stats") {
if let progress {
LabeledContent("Total Reviewed", value: "\(progress.totalReviewed)")

View File

@@ -5,6 +5,7 @@ import SwiftData
struct VerbDetailView: View {
@Environment(\.modelContext) private var modelContext
@Environment(VerbExampleCache.self) private var exampleCache
@Environment(ReflexiveVerbStore.self) private var reflexiveStore
@State private var speechService = SpeechService()
let verb: Verb
@State private var selectedTense: TenseInfo = TenseInfo.all[0]
@@ -33,6 +34,10 @@ struct VerbDetailView: View {
ReferenceStore(context: modelContext).fetchForms(verbId: verb.id, tenseId: selectedTense.id)
}
private var reflexiveEntries: [ReflexiveVerb] {
reflexiveStore.entries(for: verb.infinitive)
}
var body: some View {
List {
Section {
@@ -46,6 +51,10 @@ struct VerbDetailView: View {
Text("Info")
}
if !reflexiveEntries.isEmpty {
reflexiveSection
}
Section {
Picker("Tense", selection: $selectedTense) {
ForEach(TenseInfo.all) { tense in
@@ -106,6 +115,38 @@ struct VerbDetailView: View {
}
}
// MARK: - Reflexive
private var reflexiveSection: some View {
Section {
ForEach(Array(reflexiveEntries.enumerated()), id: \.offset) { _, entry in
VStack(alignment: .leading, spacing: 4) {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(entry.infinitive)
.font(.body.weight(.semibold))
.italic()
if let hint = entry.usageHint, !hint.isEmpty {
Text(hint)
.font(.caption.weight(.medium))
.foregroundStyle(.tint)
}
}
Text(entry.english)
.font(.caption)
.foregroundStyle(.secondary)
}
.padding(.vertical, 2)
}
} header: {
Text("Reflexive")
} footer: {
if reflexiveEntries.contains(where: { $0.usageHint != nil }) {
Text("Highlighted words are prepositions or phrases this verb commonly pairs with.")
.font(.caption2)
}
}
}
// MARK: - Examples
@ViewBuilder
@@ -198,4 +239,5 @@ struct VerbDetailView: View {
}
.modelContainer(for: [Verb.self, VerbForm.self], inMemory: true)
.environment(VerbExampleCache())
.environment(ReflexiveVerbStore())
}

View File

@@ -22,11 +22,13 @@ enum IrregularityCategory: String, CaseIterable, Identifiable {
struct VerbListView: View {
@Environment(\.modelContext) private var modelContext
@Environment(ReflexiveVerbStore.self) private var reflexiveStore
@State private var verbs: [Verb] = []
@State private var irregularityByVerbId: [Int: Set<IrregularityCategory>] = [:]
@State private var searchText = ""
@State private var selectedLevel: String?
@State private var selectedIrregularity: IrregularityCategory?
@State private var reflexiveOnly: Bool = false
@State private var selectedVerb: Verb?
private var filteredVerbs: [Verb] {
@@ -40,6 +42,9 @@ struct VerbListView: View {
return category == .anyIrregular ? !cats.isEmpty : cats.contains(category)
}
}
if reflexiveOnly {
result = result.filter { reflexiveStore.isReflexive(baseInfinitive: $0.infinitive) }
}
if !searchText.isEmpty {
let query = searchText.lowercased()
result = result.filter {
@@ -98,6 +103,14 @@ struct VerbListView: View {
}
}
}
Section("Reflexive") {
Button {
reflexiveOnly.toggle()
} label: {
Label("Reflexive verbs only", systemImage: reflexiveOnly ? "checkmark" : "arrow.triangle.2.circlepath")
}
}
} label: {
Label("Filter", systemImage: hasActiveFilter ? "line.3.horizontal.decrease.circle.fill" : "line.3.horizontal.decrease.circle")
}
@@ -115,7 +128,7 @@ struct VerbListView: View {
}
private var hasActiveFilter: Bool {
selectedLevel != nil || selectedIrregularity != nil
selectedLevel != nil || selectedIrregularity != nil || reflexiveOnly
}
@ViewBuilder
@@ -131,6 +144,11 @@ struct VerbListView: View {
selectedIrregularity = nil
}
}
if reflexiveOnly {
filterChip(text: "Reflexive", systemImage: "arrow.triangle.2.circlepath") {
reflexiveOnly = false
}
}
Spacer()
Text("\(filteredVerbs.count)")
.font(.caption.monospacedDigit())

View File

@@ -0,0 +1,104 @@
[
{"infinitive": "aburrirse", "baseInfinitive": "aburrir", "english": "to get bored"},
{"infinitive": "acercarse", "baseInfinitive": "acercar", "english": "to get close to", "usageHint": "a"},
{"infinitive": "acordarse", "baseInfinitive": "acordar", "english": "to remember", "usageHint": "de"},
{"infinitive": "acostarse", "baseInfinitive": "acostar", "english": "to lay down / to go to bed"},
{"infinitive": "acostumbrarse", "baseInfinitive": "acostumbrar", "english": "to get used to", "usageHint": "a"},
{"infinitive": "afeitarse", "baseInfinitive": "afeitar", "english": "to shave"},
{"infinitive": "alegrarse", "baseInfinitive": "alegrar", "english": "to be glad / happy / pleased"},
{"infinitive": "alejarse", "baseInfinitive": "alejar", "english": "to get away from", "usageHint": "de"},
{"infinitive": "animarse", "baseInfinitive": "animar", "english": "to cheer up / to dare to do something", "usageHint": "a"},
{"infinitive": "apurarse", "baseInfinitive": "apurar", "english": "to hurry"},
{"infinitive": "aprovecharse", "baseInfinitive": "aprovechar", "english": "to take advantage of", "usageHint": "de"},
{"infinitive": "asustarse", "baseInfinitive": "asustar", "english": "to get or become afraid"},
{"infinitive": "atreverse", "baseInfinitive": "atrever", "english": "to dare to", "usageHint": "a"},
{"infinitive": "bañarse", "baseInfinitive": "bañar", "english": "to take a bath / shower"},
{"infinitive": "burlarse", "baseInfinitive": "burlar", "english": "to make fun of", "usageHint": "de"},
{"infinitive": "caerse", "baseInfinitive": "caer", "english": "to fall down"},
{"infinitive": "calmarse", "baseInfinitive": "calmar", "english": "to calm down"},
{"infinitive": "cansarse", "baseInfinitive": "cansar", "english": "to get tired (of)", "usageHint": "(de)"},
{"infinitive": "casarse", "baseInfinitive": "casar", "english": "to marry", "usageHint": "con"},
{"infinitive": "cepillarse", "baseInfinitive": "cepillar", "english": "to brush (hair, teeth)"},
{"infinitive": "deprimirse", "baseInfinitive": "deprimir", "english": "to become depressed"},
{"infinitive": "conformarse", "baseInfinitive": "conformar", "english": "to resign oneself to", "usageHint": "con"},
{"infinitive": "volverse", "baseInfinitive": "volver", "english": "to become / to turn into / to return"},
{"infinitive": "darse", "baseInfinitive": "dar", "english": "to realize", "usageHint": "cuenta de"},
{"infinitive": "dedicarse", "baseInfinitive": "dedicar", "english": "to dedicate oneself to / to do for a living", "usageHint": "a"},
{"infinitive": "despedirse", "baseInfinitive": "despedir", "english": "to say goodbye", "usageHint": "(de)"},
{"infinitive": "despertarse", "baseInfinitive": "despertar", "english": "to wake up"},
{"infinitive": "desvestirse", "baseInfinitive": "desvestir", "english": "to undress"},
{"infinitive": "dirigirse", "baseInfinitive": "dirigir", "english": "to go to / make one's way toward / to address", "usageHint": "a"},
{"infinitive": "hacerse", "baseInfinitive": "hacer", "english": "to become / to pretend"},
{"infinitive": "divertirse", "baseInfinitive": "divertir", "english": "to have fun"},
{"infinitive": "dormirse", "baseInfinitive": "dormir", "english": "to fall asleep / to oversleep"},
{"infinitive": "ducharse", "baseInfinitive": "duchar", "english": "to shower"},
{"infinitive": "echarse", "baseInfinitive": "echar", "english": "to begin (usually suddenly) to do something / to break into", "usageHint": "a"},
{"infinitive": "enamorarse", "baseInfinitive": "enamorar", "english": "to fall in love with", "usageHint": "de"},
{"infinitive": "encargarse", "baseInfinitive": "encargar", "english": "to take charge of or be responsible for", "usageHint": "de"},
{"infinitive": "encogerse", "baseInfinitive": "encoger", "english": "to shrug (shoulders)", "usageHint": "(de hombros)"},
{"infinitive": "encontrarse", "baseInfinitive": "encontrar", "english": "to meet with / to run into someone", "usageHint": "(con)"},
{"infinitive": "enfermarse", "baseInfinitive": "enfermar", "english": "to get sick"},
{"infinitive": "enojarse", "baseInfinitive": "enojar", "english": "to get or become angry"},
{"infinitive": "enterarse", "baseInfinitive": "enterar", "english": "to find out, to realize", "usageHint": "de"},
{"infinitive": "exponerse", "baseInfinitive": "exponer", "english": "to expose oneself to or run the risk of", "usageHint": "a"},
{"infinitive": "fijarse", "baseInfinitive": "fijar", "english": "to pay attention to / to take a look"},
{"infinitive": "jugarse", "baseInfinitive": "jugar", "english": "to risk"},
{"infinitive": "lastimarse", "baseInfinitive": "lastimar", "english": "to get hurt or hurt oneself"},
{"infinitive": "lavarse", "baseInfinitive": "lavar", "english": "to wash (a body part)"},
{"infinitive": "levantarse", "baseInfinitive": "levantar", "english": "to get up"},
{"infinitive": "maquillarse", "baseInfinitive": "maquillar", "english": "to put makeup on"},
{"infinitive": "meterse", "baseInfinitive": "meter", "english": "to get into / to pick on / to pick a fight with", "usageHint": "en / con"},
{"infinitive": "motivarse", "baseInfinitive": "motivar", "english": "to become or get motivated to"},
{"infinitive": "moverse", "baseInfinitive": "mover", "english": "to move oneself"},
{"infinitive": "mudarse", "baseInfinitive": "mudar", "english": "to move (change residence)"},
{"infinitive": "negarse", "baseInfinitive": "negar", "english": "to refuse to", "usageHint": "a"},
{"infinitive": "obsesionarse", "baseInfinitive": "obsesionar", "english": "to be or get obsessed with", "usageHint": "con"},
{"infinitive": "ocuparse", "baseInfinitive": "ocupar", "english": "to look after", "usageHint": "de"},
{"infinitive": "olvidarse", "baseInfinitive": "olvidar", "english": "to forget", "usageHint": "de"},
{"infinitive": "parecerse", "baseInfinitive": "parecer", "english": "to look like someone or something", "usageHint": "a"},
{"infinitive": "peinarse", "baseInfinitive": "peinar", "english": "to comb your hair"},
{"infinitive": "ponerse", "baseInfinitive": "poner", "english": "to put on (clothing) / to get or become"},
{"infinitive": "ponerse", "baseInfinitive": "poner", "english": "to come to an agreement with someone", "usageHint": "de acuerdo"},
{"infinitive": "preocuparse", "baseInfinitive": "preocupar", "english": "to worry about", "usageHint": "por"},
{"infinitive": "prepararse", "baseInfinitive": "preparar", "english": "to prepare to"},
{"infinitive": "probarse", "baseInfinitive": "probar", "english": "to try on"},
{"infinitive": "quebrarse", "baseInfinitive": "quebrar", "english": "to break (an arm, leg, etc.)"},
{"infinitive": "quejarse", "baseInfinitive": "quejar", "english": "to complain about", "usageHint": "de"},
{"infinitive": "quedarse", "baseInfinitive": "quedar", "english": "to remain / to stay"},
{"infinitive": "quemarse", "baseInfinitive": "quemar", "english": "to burn oneself / one's body"},
{"infinitive": "quitarse", "baseInfinitive": "quitar", "english": "to take off (clothing, etc.)"},
{"infinitive": "reírse", "baseInfinitive": "reír", "english": "to laugh about", "usageHint": "de"},
{"infinitive": "resignarse", "baseInfinitive": "resignar", "english": "to resign oneself to", "usageHint": "a"},
{"infinitive": "romperse", "baseInfinitive": "romper", "english": "to break (an arm, leg, etc.)"},
{"infinitive": "secarse", "baseInfinitive": "secar", "english": "to dry (a body part)"},
{"infinitive": "sentarse", "baseInfinitive": "sentar", "english": "to sit down"},
{"infinitive": "sentirse", "baseInfinitive": "sentir", "english": "to feel"},
{"infinitive": "servirse", "baseInfinitive": "servir", "english": "to help oneself to (food)"},
{"infinitive": "suicidarse", "baseInfinitive": "suicidar", "english": "to commit suicide"},
{"infinitive": "tratarse", "baseInfinitive": "tratar", "english": "to be about", "usageHint": "de"},
{"infinitive": "vestirse", "baseInfinitive": "vestir", "english": "to get dressed"},
{"infinitive": "marearse", "baseInfinitive": "marear", "english": "to get sick, to get dizzy"},
{"infinitive": "irse", "baseInfinitive": "ir", "english": "to leave"},
{"infinitive": "imaginarse", "baseInfinitive": "imaginar", "english": "to imagine"},
{"infinitive": "preguntarse", "baseInfinitive": "preguntar", "english": "to wonder"},
{"infinitive": "llamarse", "baseInfinitive": "llamar", "english": "to be called"},
{"infinitive": "verse", "baseInfinitive": "ver", "english": "to look or appear"},
{"infinitive": "distraerse", "baseInfinitive": "distraer", "english": "to get distracted"},
{"infinitive": "concentrarse", "baseInfinitive": "concentrar", "english": "to focus"},
{"infinitive": "rendirse", "baseInfinitive": "rendir", "english": "to give up"},
{"infinitive": "relajarse", "baseInfinitive": "relajar", "english": "to relax"},
{"infinitive": "merecerse", "baseInfinitive": "merecer", "english": "to deserve"},
{"infinitive": "suponerse", "baseInfinitive": "suponer", "english": "to suppose"},
{"infinitive": "conectarse", "baseInfinitive": "conectar", "english": "to connect"},
{"infinitive": "destacarse", "baseInfinitive": "destacar", "english": "to stand out"},
{"infinitive": "recibirse", "baseInfinitive": "recibir", "english": "to graduate"},
{"infinitive": "graduarse", "baseInfinitive": "graduar", "english": "to graduate"},
{"infinitive": "perderse", "baseInfinitive": "perder", "english": "to get lost"},
{"infinitive": "cambiarse", "baseInfinitive": "cambiar", "english": "to change (clothing)", "usageHint": "(de ropa)"},
{"infinitive": "adaptarse", "baseInfinitive": "adaptar", "english": "to adapt, to adjust", "usageHint": "a"},
{"infinitive": "salirse", "baseInfinitive": "salir", "english": "to get away with", "usageHint": "con (la suya)"},
{"infinitive": "subirse", "baseInfinitive": "subir", "english": "to get on (the bus, etc.)", "usageHint": "a"},
{"infinitive": "tranquilizarse", "baseInfinitive": "tranquilizar", "english": "to relax"},
{"infinitive": "equivocarse", "baseInfinitive": "equivocar", "english": "to get something wrong / confused"},
{"infinitive": "confundirse", "baseInfinitive": "confundir", "english": "to get something wrong / confused"}
]

View File

@@ -0,0 +1,23 @@
import Foundation
/// A single entry from the curated "100 most common reflexive verbs" list
/// (Gitea issue #28). Sourced from spanishwithdaniel.com.
///
/// `baseInfinitive` is the stem without the reflexive "-se" suffix, used to
/// match this entry to the app's Verb records (which store bare infinitives).
/// `usageHint` captures trailing prepositions or set-phrase completions e.g.,
/// "a" for `acercarse a`, "de acuerdo" for `ponerse de acuerdo`. Nil when the
/// reflexive form has no commonly paired preposition.
public struct ReflexiveVerb: Codable, Hashable, Sendable {
public let infinitive: String
public let baseInfinitive: String
public let english: String
public let usageHint: String?
public init(infinitive: String, baseInfinitive: String, english: String, usageHint: String? = nil) {
self.infinitive = infinitive
self.baseInfinitive = baseInfinitive
self.english = english
self.usageHint = usageHint
}
}

View File

@@ -47,6 +47,8 @@ targets:
buildPhase: resources
- path: Conjuga/course_data.json
buildPhase: resources
- path: Conjuga/reflexive_verbs.json
buildPhase: resources
info:
path: Conjuga/Info.plist
properties: