diff --git a/Sources/Core/Localizable.xcstrings b/Sources/Core/Localizable.xcstrings index 14d73ac..3aef6b3 100644 --- a/Sources/Core/Localizable.xcstrings +++ b/Sources/Core/Localizable.xcstrings @@ -243,6 +243,22 @@ } } }, + "Cancel" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "إلغاء" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "İptal" + } + } + } + }, "City" : { "extractionState" : "stale", "localizations" : { @@ -1181,6 +1197,22 @@ } } }, + "Search" : { + "localizations" : { + "ar" : { + "stringUnit" : { + "state" : "translated", + "value" : "بحث" + } + }, + "tr" : { + "stringUnit" : { + "state" : "translated", + "value" : "Ara" + } + } + } + }, "Select…" : { "localizations" : { "ar" : { diff --git a/Sources/Shared/LocationPickerModel.swift b/Sources/Shared/LocationPickerModel.swift index bcd8d40..445f1d5 100644 --- a/Sources/Shared/LocationPickerModel.swift +++ b/Sources/Shared/LocationPickerModel.swift @@ -29,6 +29,21 @@ final class LocationPickerModel: ObservableObject { districts.first { $0.id == selectedDistrictId } } + /// Display name of the currently selected country (for the picker row). + var selectedCountryName: String? { + countries.first { $0.id == selectedCountryId }?.name + } + + /// Countries sorted by name (locale-aware), optionally filtered by a search term. + func countries(matching search: String) -> [Country] { + let sorted = countries.sorted { + $0.name.localizedCaseInsensitiveCompare($1.name) == .orderedAscending + } + let term = search.trimmingCharacters(in: .whitespacesAndNewlines) + guard !term.isEmpty else { return sorted } + return sorted.filter { $0.name.localizedCaseInsensitiveContains(term) } + } + func loadCountriesIfNeeded() { guard countries.isEmpty else { return } isLoading = true diff --git a/Sources/Shared/LocationPickerView.swift b/Sources/Shared/LocationPickerView.swift index e5a555d..d429b43 100644 --- a/Sources/Shared/LocationPickerView.swift +++ b/Sources/Shared/LocationPickerView.swift @@ -5,17 +5,26 @@ struct LocationPickerView: View { @ObservedObject var store: PrayerStore @StateObject private var model = LocationPickerModel() @Environment(\.theme) private var theme + @State private var showCountryPicker = false var body: some View { Group { LabeledContent("Current", value: store.locationName) - Picker("Country", selection: Binding( - get: { model.selectedCountryId }, - set: { model.selectCountry($0) } - )) { - ForEach(model.countries) { Text($0.name).tag($0.id) } + Button { + showCountryPicker = true + } label: { + LabeledContent("Country") { + HStack(spacing: 6) { + Text(model.selectedCountryName ?? "Select…") + Image(systemName: "chevron.up.chevron.down") + .font(.caption2) + .foregroundStyle(theme.muted) + } + } } + .buttonStyle(.plain) + .disabled(model.countries.isEmpty) Picker("City", selection: Binding( get: { model.selectedCityId }, @@ -44,5 +53,79 @@ struct LocationPickerView: View { .disabled(model.selectedDistrict == nil) } .task { model.loadCountriesIfNeeded() } + .sheet(isPresented: $showCountryPicker) { + CountryPickerSheet( + model: model, + selectedId: model.selectedCountryId, + onSelect: { model.selectCountry($0.id) } + ) + } + } +} + +/// Searchable, name-sorted country list shown as a sheet. +private struct CountryPickerSheet: View { + @ObservedObject var model: LocationPickerModel + let selectedId: String + let onSelect: (Country) -> Void + + @Environment(\.dismiss) private var dismiss + @Environment(\.theme) private var theme + @State private var search = "" + + var body: some View { + NavigationStack { + countryList + .navigationTitle("Country") + #if os(iOS) + .navigationBarTitleDisplayMode(.inline) + .searchable(text: $search, prompt: "Search") + #endif + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Cancel") { dismiss() } + } + } + } + #if os(macOS) + // A macOS sheet has no intrinsic size; give the searchable list room. + .frame(minWidth: 360, idealWidth: 420, minHeight: 420, idealHeight: 520) + #endif + } + + private var countryList: some View { + List(model.countries(matching: search)) { country in + Button { + onSelect(country) + dismiss() + } label: { + HStack { + Text(country.name) + .foregroundStyle(theme.text) + Spacer() + if country.id == selectedId { + Image(systemName: "checkmark") + .foregroundStyle(theme.accent) + } + } + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + #if os(macOS) + // macOS sheets don't render `.searchable`'s field reliably, so use an + // explicit search field pinned above the list. + .safeAreaInset(edge: .top) { + HStack(spacing: 6) { + Image(systemName: "magnifyingglass").foregroundStyle(theme.muted) + TextField("Search", text: $search) + .textFieldStyle(.plain) + } + .padding(8) + .background(theme.surface, in: RoundedRectangle(cornerRadius: 8)) + .padding(.horizontal) + .padding(.top, 8) + } + #endif } }