mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #592 from omnivore-app/feature/searchbar-ios
Search Bar and CoreData Updates
This commit is contained in:
commit
1fc1722698
15 changed files with 268 additions and 99 deletions
|
|
@ -26,16 +26,6 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
|
|||
.refreshable {
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.searchable(
|
||||
text: $viewModel.searchTerm
|
||||
) {
|
||||
if viewModel.searchTerm.isEmpty {
|
||||
Text("Inbox").searchCompletion("in:inbox ")
|
||||
Text("All").searchCompletion("in:all ")
|
||||
Text("Archived").searchCompletion("in:archive ")
|
||||
Text("Files").searchCompletion("type:file ")
|
||||
}
|
||||
}
|
||||
.onChange(of: viewModel.searchTerm) { _ in
|
||||
// Maybe we should debounce this, but
|
||||
// it feels like it works ok without
|
||||
|
|
@ -44,7 +34,7 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
|
|||
.onChange(of: viewModel.selectedLabels) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.onSubmit(of: .search) {
|
||||
.onChange(of: viewModel.appliedFilter) { _ in
|
||||
loadItems(isRefresh: true)
|
||||
}
|
||||
.sheet(item: $viewModel.itemUnderLabelEdit) { item in
|
||||
|
|
@ -135,9 +125,23 @@ private let enableGrid = UIDevice.isIPad || FeatureFlag.enableGridCardsOnPhone
|
|||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
SearchBar(searchTerm: $viewModel.searchTerm)
|
||||
|
||||
ZStack(alignment: .bottom) {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
Menu(
|
||||
content: {
|
||||
ForEach(LinkedItemFilter.allCases, id: \.self) { filter in
|
||||
Button(filter.displayName, action: { viewModel.appliedFilter = filter.rawValue })
|
||||
}
|
||||
},
|
||||
label: {
|
||||
TextChipButton.makeFilterButton(
|
||||
title: LinkedItemFilter(rawValue: viewModel.appliedFilter)?.displayName ?? "Filter"
|
||||
)
|
||||
}
|
||||
)
|
||||
TextChipButton.makeAddLabelButton {
|
||||
showLabelsSheet = true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,9 @@ import Views
|
|||
@Published var selectedLinkItem: LinkedItem?
|
||||
@Published var showLoadingBar = false
|
||||
|
||||
@AppStorage(UserDefaultKey.lastSelectedLinkedItemFilter.rawValue)
|
||||
var appliedFilter = LinkedItemFilter.inbox.rawValue
|
||||
|
||||
var cursor: String?
|
||||
|
||||
// These are used to make sure we handle search result
|
||||
|
|
@ -84,11 +87,13 @@ import Views
|
|||
cursor = queryResult.cursor
|
||||
await dataService.prefetchPages(itemIDs: newItems.map(\.unwrappedID))
|
||||
showLoadingBar = false
|
||||
} else if searchTermIsEmpty {
|
||||
} else if searchTerm.replacingOccurrences(of: " ", with: "").isEmpty {
|
||||
await dataService.viewContext.perform {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItem> = LinkedItem.fetchRequest()
|
||||
fetchRequest.sortDescriptors = [NSSortDescriptor(keyPath: \LinkedItem.savedAt, ascending: false)]
|
||||
fetchRequest.predicate = self.itemRequestPredicate
|
||||
if let predicate = LinkedItemFilter(rawValue: self.appliedFilter)?.predicate {
|
||||
fetchRequest.predicate = predicate
|
||||
}
|
||||
// // TODO: Filter on label
|
||||
|
||||
if let fetchedItems = try? dataService.viewContext.fetch(fetchRequest) {
|
||||
|
|
@ -101,49 +106,6 @@ import Views
|
|||
}
|
||||
}
|
||||
|
||||
private var itemRequestPredicate: NSPredicate {
|
||||
let undeletedPredicate = NSPredicate(
|
||||
format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue)
|
||||
)
|
||||
|
||||
if searchTerm.contains("in:all") {
|
||||
// include everything undeleted
|
||||
return undeletedPredicate
|
||||
}
|
||||
|
||||
if searchTerm.contains("in:archive") {
|
||||
let inArchivePredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: true) as NSNumber
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate])
|
||||
}
|
||||
|
||||
if searchTerm.contains("type:file") {
|
||||
// include pdf only
|
||||
let isPDFPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItem.contentReader), "PDF"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate])
|
||||
}
|
||||
|
||||
// default to "in:inbox" (non-archived items)
|
||||
let notInArchivePredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate])
|
||||
}
|
||||
|
||||
// Exclude filters when testing if user has enetered a search term
|
||||
private var searchTermIsEmpty: Bool {
|
||||
searchTerm
|
||||
.replacingOccurrences(of: "in:inbox", with: "")
|
||||
.replacingOccurrences(of: "in:all", with: "")
|
||||
.replacingOccurrences(of: "in:archive", with: "")
|
||||
.replacingOccurrences(of: "type:file", with: "")
|
||||
.replacingOccurrences(of: " ", with: "")
|
||||
.isEmpty
|
||||
}
|
||||
|
||||
func setLinkArchived(dataService: DataService, objectID: NSManagedObjectID, archived: Bool) {
|
||||
// TODO: remove this by making list always fetch from Coredata
|
||||
guard let itemIndex = items.firstIndex(where: { $0.objectID == objectID }) else { return }
|
||||
|
|
@ -182,12 +144,13 @@ import Views
|
|||
isLoading = false
|
||||
}
|
||||
|
||||
private var searchQuery: String? {
|
||||
if searchTerm.isEmpty, selectedLabels.isEmpty {
|
||||
return nil
|
||||
}
|
||||
private var searchQuery: String {
|
||||
let filter = LinkedItemFilter(rawValue: appliedFilter) ?? .inbox
|
||||
var query = "\(filter.queryString)"
|
||||
|
||||
var query = searchTerm
|
||||
if !searchTerm.isEmpty {
|
||||
query.append(" \(searchTerm)")
|
||||
}
|
||||
|
||||
if !selectedLabels.isEmpty {
|
||||
query.append(" label:")
|
||||
|
|
|
|||
|
|
@ -64,7 +64,7 @@ import Views
|
|||
}
|
||||
|
||||
func saveItemLabelChanges(itemID: String, dataService: DataService) {
|
||||
dataService.updateItemLabels(itemID: itemID, labelNames: selectedLabels.map(\.unwrappedName))
|
||||
dataService.updateItemLabels(itemID: itemID, labelIDs: selectedLabels.map(\.unwrappedID))
|
||||
}
|
||||
|
||||
func addLabelToItem(_ label: LinkedItemLabel) {
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import WebKit
|
|||
@Binding var increaseFontActionID: UUID?
|
||||
@Binding var decreaseFontActionID: UUID?
|
||||
@Binding var annotationSaveTransactionID: UUID?
|
||||
@Binding var showNavBarActionID: UUID?
|
||||
@Binding var annotation: String
|
||||
|
||||
func makeCoordinator() -> WebReaderCoordinator {
|
||||
|
|
@ -75,6 +76,11 @@ import WebKit
|
|||
(webView as? WebView)?.decreaseFontSize()
|
||||
}
|
||||
|
||||
if showNavBarActionID != context.coordinator.previousShowNavBarActionID {
|
||||
context.coordinator.previousShowNavBarActionID = showNavBarActionID
|
||||
context.coordinator.showNavBar()
|
||||
}
|
||||
|
||||
// If the webview had been terminated `needsReload` will have been set to true
|
||||
if context.coordinator.needsReload {
|
||||
loadContent(webView: webView)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import WebKit
|
|||
@State var increaseFontActionID: UUID?
|
||||
@State var decreaseFontActionID: UUID?
|
||||
@State var annotationSaveTransactionID: UUID?
|
||||
@State var showNavBarActionID: UUID?
|
||||
@State var annotation = String()
|
||||
|
||||
@EnvironmentObject var dataService: DataService
|
||||
|
|
@ -154,8 +155,15 @@ import WebKit
|
|||
increaseFontActionID: $increaseFontActionID,
|
||||
decreaseFontActionID: $decreaseFontActionID,
|
||||
annotationSaveTransactionID: $annotationSaveTransactionID,
|
||||
showNavBarActionID: $showNavBarActionID,
|
||||
annotation: $annotation
|
||||
)
|
||||
.onTapGesture {
|
||||
withAnimation {
|
||||
navBarVisibilityRatio = 1
|
||||
showNavBarActionID = UUID()
|
||||
}
|
||||
}
|
||||
.sheet(item: $safariWebLink) {
|
||||
SafariView(url: $0.url)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ final class WebReaderCoordinator: NSObject {
|
|||
var lastSavedAnnotationID: UUID?
|
||||
var previousIncreaseFontActionID: UUID?
|
||||
var previousDecreaseFontActionID: UUID?
|
||||
var previousShowNavBarActionID: UUID?
|
||||
var updateNavBarVisibilityRatio: (Double) -> Void = { _ in }
|
||||
private var yOffsetAtStartOfDrag: Double?
|
||||
private var lastYOffset: Double = 0
|
||||
|
|
@ -33,6 +34,10 @@ final class WebReaderCoordinator: NSObject {
|
|||
updateNavBarVisibilityRatio(navBarVisibilityRatio)
|
||||
}
|
||||
}
|
||||
|
||||
func showNavBar() {
|
||||
isNavBarHidden = false
|
||||
}
|
||||
}
|
||||
|
||||
extension WebReaderCoordinator: WKScriptMessageHandler {
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@
|
|||
<attribute name="slug" attributeType="String"/>
|
||||
<attribute name="title" attributeType="String"/>
|
||||
<relationship name="highlights" toMany="YES" deletionRule="Cascade" destinationEntity="Highlight" inverseName="linkedItem" inverseEntity="Highlight"/>
|
||||
<relationship name="labels" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItemLabel"/>
|
||||
<relationship name="labels" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItemLabel" inverseName="linkedItems" inverseEntity="LinkedItemLabel"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
|
|
@ -55,6 +55,7 @@
|
|||
<attribute name="labelDescription" optional="YES" attributeType="String"/>
|
||||
<attribute name="name" attributeType="String"/>
|
||||
<attribute name="serverSyncStatus" attributeType="Integer 64" defaultValueString="NO" usesScalarValueType="YES"/>
|
||||
<relationship name="linkedItems" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="LinkedItem" inverseName="labels" inverseEntity="LinkedItem"/>
|
||||
<uniquenessConstraints>
|
||||
<uniquenessConstraint>
|
||||
<constraint value="id"/>
|
||||
|
|
@ -85,7 +86,7 @@
|
|||
<elements>
|
||||
<element name="Highlight" positionX="27" positionY="225" width="128" height="224"/>
|
||||
<element name="LinkedItem" positionX="-18" positionY="63" width="128" height="344"/>
|
||||
<element name="LinkedItemLabel" positionX="-36" positionY="18" width="128" height="119"/>
|
||||
<element name="LinkedItemLabel" positionX="-36" positionY="18" width="128" height="134"/>
|
||||
<element name="NewsletterEmail" positionX="0" positionY="180" width="128" height="74"/>
|
||||
<element name="Viewer" positionX="45" positionY="234" width="128" height="89"/>
|
||||
</elements>
|
||||
|
|
|
|||
83
apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift
Normal file
83
apple/OmnivoreKit/Sources/Models/LinkedItemFilter.swift
Normal file
|
|
@ -0,0 +1,83 @@
|
|||
import Foundation
|
||||
|
||||
public enum LinkedItemFilter: String, CaseIterable {
|
||||
case inbox
|
||||
case readlater
|
||||
case newsletters
|
||||
case all
|
||||
case archived
|
||||
case files
|
||||
}
|
||||
|
||||
public extension LinkedItemFilter {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .inbox:
|
||||
return "Inbox"
|
||||
case .readlater:
|
||||
return "Read Later"
|
||||
case .newsletters:
|
||||
return "Newsletters"
|
||||
case .all:
|
||||
return "All"
|
||||
case .archived:
|
||||
return "Archived"
|
||||
case .files:
|
||||
return "Files"
|
||||
}
|
||||
}
|
||||
|
||||
var queryString: String {
|
||||
switch self {
|
||||
case .inbox:
|
||||
return "in:inbox"
|
||||
case .readlater:
|
||||
return "in:inbox -label:Newsletter"
|
||||
case .newsletters:
|
||||
return "in:inbox label:Newsletter"
|
||||
case .all:
|
||||
return "in:all"
|
||||
case .archived:
|
||||
return "in:archive"
|
||||
case .files:
|
||||
return "type:file"
|
||||
}
|
||||
}
|
||||
|
||||
var predicate: NSPredicate {
|
||||
let undeletedPredicate = NSPredicate(
|
||||
format: "%K != %i", #keyPath(LinkedItem.serverSyncStatus), Int64(ServerSyncStatus.needsDeletion.rawValue)
|
||||
)
|
||||
let notInArchivePredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: false) as NSNumber
|
||||
)
|
||||
|
||||
switch self {
|
||||
case .inbox:
|
||||
// non-archived items
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate])
|
||||
case .readlater:
|
||||
// non-archived or deleted items without the Newsletter label
|
||||
let nonNewsletterLabelPredicate = NSPredicate(format: "NOT SUBQUERY(labels, $label, $label.name == \"Newsletter\") .@count > 0")
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, notInArchivePredicate, nonNewsletterLabelPredicate])
|
||||
case .newsletters:
|
||||
// non-archived or deleted items with the Newsletter label
|
||||
let newsletterLabelPredicate = NSPredicate(format: "SUBQUERY(labels, $label, $label.name == \"Newsletter\").@count > 0")
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [notInArchivePredicate, newsletterLabelPredicate])
|
||||
case .all:
|
||||
// include everything undeleted
|
||||
return undeletedPredicate
|
||||
case .archived:
|
||||
let inArchivePredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItem.isArchived), Int(truncating: true) as NSNumber
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, inArchivePredicate])
|
||||
case .files:
|
||||
// include pdf only
|
||||
let isPDFPredicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItem.contentReader), "PDF"
|
||||
)
|
||||
return NSCompoundPredicate(andPredicateWithSubpredicates: [undeletedPredicate, isPDFPredicate])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,7 @@ import CoreData
|
|||
import Foundation
|
||||
import Models
|
||||
import OSLog
|
||||
import Utils
|
||||
|
||||
let logger = Logger(subsystem: "app.omnivore", category: "data-service")
|
||||
|
||||
|
|
@ -13,8 +14,8 @@ public final class DataService: ObservableObject {
|
|||
public let appEnvironment: AppEnvironment
|
||||
let networker: Networker
|
||||
|
||||
let persistentContainer: PersistentContainer
|
||||
let backgroundContext: NSManagedObjectContext
|
||||
var persistentContainer: PersistentContainer
|
||||
var backgroundContext: NSManagedObjectContext
|
||||
var subscriptions = Set<AnyCancellable>()
|
||||
|
||||
public var viewContext: NSManagedObjectContext {
|
||||
|
|
@ -28,9 +29,13 @@ public final class DataService: ObservableObject {
|
|||
self.backgroundContext = persistentContainer.newBackgroundContext()
|
||||
backgroundContext.mergePolicy = NSMergePolicy.mergeByPropertyObjectTrump
|
||||
|
||||
persistentContainer.loadPersistentStores { _, error in
|
||||
if let error = error {
|
||||
fatalError("Core Data store failed to load with error: \(error)")
|
||||
if isFirstTimeRunningNewAppVersion() {
|
||||
resetCoreData()
|
||||
} else {
|
||||
persistentContainer.loadPersistentStores { _, error in
|
||||
if let error = error {
|
||||
fatalError("Core Data store failed to load with error: \(error)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -49,4 +54,38 @@ public final class DataService: ObservableObject {
|
|||
fatalError("Unable to write to Keychain: \(error)")
|
||||
}
|
||||
}
|
||||
|
||||
private func resetCoreData() {
|
||||
let storeContainer =
|
||||
persistentContainer.persistentStoreCoordinator
|
||||
|
||||
do {
|
||||
for store in storeContainer.persistentStores {
|
||||
try storeContainer.destroyPersistentStore(
|
||||
at: store.url!,
|
||||
ofType: store.type,
|
||||
options: nil
|
||||
)
|
||||
}
|
||||
persistentContainer = PersistentContainer.make()
|
||||
persistentContainer.loadPersistentStores { _, error in
|
||||
if let error = error {
|
||||
fatalError("Core Data store failed to load with error: \(error)")
|
||||
}
|
||||
}
|
||||
backgroundContext = persistentContainer.newBackgroundContext()
|
||||
} catch {
|
||||
logger.debug("Failed to reset core data stores")
|
||||
}
|
||||
}
|
||||
|
||||
private func isFirstTimeRunningNewAppVersion() -> Bool {
|
||||
let appVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString")
|
||||
guard let appVersion = appVersion as? String else { return false }
|
||||
|
||||
let lastUsedAppVersion = UserDefaults.standard.string(forKey: UserDefaultKey.lastUsedAppVersion.rawValue)
|
||||
let isFirstRun = (lastUsedAppVersion ?? "unknown") != appVersion
|
||||
UserDefaults.standard.set(appVersion, forKey: UserDefaultKey.lastUsedAppVersion.rawValue)
|
||||
return isFirstRun
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ extension DataService {
|
|||
// Update CoreData
|
||||
backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let label = LinkedItemLabel.lookup(byName: name, inContext: self.backgroundContext) else { return }
|
||||
guard let label = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) else { return }
|
||||
label.remove(inContext: self.backgroundContext)
|
||||
|
||||
// Send update to server
|
||||
|
|
@ -15,7 +15,7 @@ extension DataService {
|
|||
}
|
||||
}
|
||||
|
||||
func syncLabelDeletion(labelID: String, labelName: String) {
|
||||
func syncLabelDeletion(labelID: String, labelName _: String) {
|
||||
enum MutationResult {
|
||||
case success(labelID: String)
|
||||
case error(errorCode: Enums.DeleteLabelErrorCode)
|
||||
|
|
@ -43,7 +43,7 @@ extension DataService {
|
|||
let isSyncSuccess = data != nil
|
||||
|
||||
context.perform {
|
||||
let label = LinkedItemLabel.lookup(byName: labelName, inContext: context)
|
||||
let label = LinkedItemLabel.lookup(byID: labelID, inContext: context)
|
||||
guard let label = label else { return }
|
||||
|
||||
if isSyncSuccess {
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ import Models
|
|||
import SwiftGraphQL
|
||||
|
||||
extension DataService {
|
||||
public func updateItemLabels(itemID: String, labelNames: [String]) {
|
||||
public func updateItemLabels(itemID: String, labelIDs: [String]) {
|
||||
backgroundContext.perform { [weak self] in
|
||||
guard let self = self else { return }
|
||||
guard let linkedItem = LinkedItem.lookup(byID: itemID, inContext: self.backgroundContext) else { return }
|
||||
|
|
@ -13,12 +13,9 @@ extension DataService {
|
|||
linkedItem.removeFromLabels(existingLabels)
|
||||
}
|
||||
|
||||
var labelIDs = [String]()
|
||||
|
||||
for labelName in labelNames {
|
||||
if let labelObject = LinkedItemLabel.lookup(byName: labelName, inContext: self.backgroundContext) {
|
||||
for labelID in labelIDs {
|
||||
if let labelObject = LinkedItemLabel.lookup(byID: labelID, inContext: self.backgroundContext) {
|
||||
linkedItem.addToLabels(labelObject)
|
||||
labelIDs.append(labelObject.unwrappedID)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -29,8 +29,8 @@ struct InternalLinkedItemLabel {
|
|||
}
|
||||
|
||||
func asManagedObject(inContext context: NSManagedObjectContext) -> LinkedItemLabel {
|
||||
let existingItem = LinkedItemLabel.lookup(byName: name, inContext: context)
|
||||
let label = existingItem ?? LinkedItemLabel(entity: LinkedItemLabel.entity(), insertInto: context)
|
||||
let existingLabel = LinkedItemLabel.lookup(byID: id, inContext: context)
|
||||
let label = existingLabel ?? LinkedItemLabel(entity: LinkedItemLabel.entity(), insertInto: context)
|
||||
label.id = id
|
||||
label.name = name
|
||||
label.color = color
|
||||
|
|
@ -44,10 +44,10 @@ extension LinkedItemLabel {
|
|||
public var unwrappedID: String { id ?? "" }
|
||||
public var unwrappedName: String { name ?? "" }
|
||||
|
||||
static func lookup(byName name: String, inContext context: NSManagedObjectContext) -> LinkedItemLabel? {
|
||||
static func lookup(byID id: String, inContext context: NSManagedObjectContext) -> LinkedItemLabel? {
|
||||
let fetchRequest: NSFetchRequest<Models.LinkedItemLabel> = LinkedItemLabel.fetchRequest()
|
||||
fetchRequest.predicate = NSPredicate(
|
||||
format: "%K == %@", #keyPath(LinkedItemLabel.name), name
|
||||
format: "id == %@", id
|
||||
)
|
||||
|
||||
var label: LinkedItemLabel?
|
||||
|
|
|
|||
|
|
@ -5,4 +5,6 @@ public enum UserDefaultKey: String {
|
|||
case userHasDeniedPushPrimer
|
||||
case firebasePushToken
|
||||
case homeFeedlayoutPreference
|
||||
case lastSelectedLinkedItemFilter
|
||||
case lastUsedAppVersion
|
||||
}
|
||||
|
|
|
|||
59
apple/OmnivoreKit/Sources/Views/SearchBar.swift
Normal file
59
apple/OmnivoreKit/Sources/Views/SearchBar.swift
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
import SwiftUI
|
||||
|
||||
public struct SearchBar: View {
|
||||
@Binding var searchTerm: String
|
||||
@FocusState private var isFocused: Bool
|
||||
|
||||
public init(
|
||||
searchTerm: Binding<String>
|
||||
) {
|
||||
self._searchTerm = searchTerm
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
TextField("Search", text: $searchTerm)
|
||||
.padding(7)
|
||||
.padding(.horizontal, 25)
|
||||
.background(Color(.systemGray6))
|
||||
.cornerRadius(8)
|
||||
.focused($isFocused)
|
||||
.overlay(
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.frame(minWidth: 0, maxWidth: .infinity, alignment: .leading)
|
||||
.padding(.leading, 10)
|
||||
|
||||
if self.searchTerm != "" {
|
||||
Button(
|
||||
action: {
|
||||
self.searchTerm = ""
|
||||
},
|
||||
label: {
|
||||
Image(systemName: "multiply.circle.fill")
|
||||
.foregroundColor(.gray)
|
||||
.padding(.trailing, 8)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
.padding(.horizontal, 10)
|
||||
|
||||
if isFocused {
|
||||
Button(
|
||||
action: {
|
||||
self.searchTerm = ""
|
||||
self.isFocused = false
|
||||
},
|
||||
label: {
|
||||
Text("Cancel")
|
||||
}
|
||||
)
|
||||
.padding(.trailing, 10)
|
||||
.transition(.move(edge: .trailing))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,10 @@ public struct TextChipButton: View {
|
|||
TextChipButton(title: "Labels", color: .systemGray6, actionType: .show, onTap: onTap)
|
||||
}
|
||||
|
||||
public static func makeFilterButton(title: String) -> TextChipButton {
|
||||
TextChipButton(title: title, color: .systemGray6, actionType: .show, onTap: {})
|
||||
}
|
||||
|
||||
public static func makeShowOptionsButton(title: String, onTap: @escaping () -> Void) -> TextChipButton {
|
||||
TextChipButton(title: title, color: .appButtonBackground, actionType: .add, onTap: onTap)
|
||||
}
|
||||
|
|
@ -67,7 +71,7 @@ public struct TextChipButton: View {
|
|||
}
|
||||
}
|
||||
|
||||
init(title: String, color: Color, actionType: ActionType, onTap: @escaping () -> Void) {
|
||||
public init(title: String, color: Color, actionType: ActionType, onTap: @escaping () -> Void) {
|
||||
self.text = title
|
||||
self.color = color
|
||||
self.onTap = onTap
|
||||
|
|
@ -87,23 +91,21 @@ public struct TextChipButton: View {
|
|||
let foregroundColor: Color
|
||||
|
||||
public var body: some View {
|
||||
Button(action: onTap) {
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text(text)
|
||||
.padding(.leading, 3)
|
||||
Image(systemName: actionType.systemIconName)
|
||||
}
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(foregroundColor)
|
||||
.lineLimit(1)
|
||||
.background(Capsule().fill(color))
|
||||
|
||||
Color.clear.contentShape(Rectangle()).frame(height: 15)
|
||||
VStack(spacing: 0) {
|
||||
HStack {
|
||||
Text(text)
|
||||
.padding(.leading, 3)
|
||||
Image(systemName: actionType.systemIconName)
|
||||
}
|
||||
.contentShape(Rectangle())
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 8)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(foregroundColor)
|
||||
.lineLimit(1)
|
||||
.background(Capsule().fill(color))
|
||||
}
|
||||
.padding(.vertical, 12)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture { onTap() }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue