mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #3222 from omnivore-app/fix/ios-searchbar
iOS searchbar and visual fixes
This commit is contained in:
commit
fbc9f04e3b
19 changed files with 559 additions and 123 deletions
|
|
@ -29,7 +29,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
@State var hasHighlightMutations = false
|
||||
@State var searchPresented = false
|
||||
@State var addLinkPresented = false
|
||||
@State var settingsPresented = false
|
||||
@State var isListScrolled = false
|
||||
@State var listTitle = ""
|
||||
@State var isEditMode: EditMode = .inactive
|
||||
|
|
@ -54,7 +53,8 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
|
||||
var showFeatureCards: Bool {
|
||||
viewModel.listConfig.hasFeatureCards &&
|
||||
isEditMode == .inactive &&
|
||||
viewModel.listConfig.hasFeatureCards &&
|
||||
!viewModel.hideFeatureSection &&
|
||||
viewModel.fetcher.items.count > 0 &&
|
||||
viewModel.searchTerm.isEmpty &&
|
||||
|
|
@ -179,11 +179,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
LibraryAddLinkView()
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $settingsPresented) {
|
||||
NavigationView {
|
||||
ProfileView()
|
||||
}
|
||||
}
|
||||
.task {
|
||||
if viewModel.fetcher.items.isEmpty {
|
||||
loadItems(isRefresh: false)
|
||||
|
|
@ -210,14 +205,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
.frame(maxWidth: .infinity, alignment: .bottomLeading)
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
if UIDevice.isIPad, viewModel.folder == "inbox" {
|
||||
Button(action: { addLinkPresented = true }, label: {
|
||||
Label("Add Link", systemImage: "plus")
|
||||
})
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: UIDevice.isIPhone ? .barLeading : .barTrailing) {
|
||||
if enableGrid {
|
||||
Button(
|
||||
|
|
@ -232,29 +219,31 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
Button(
|
||||
action: { searchPresented = true },
|
||||
label: {
|
||||
Image(systemName: "magnifyingglass")
|
||||
Image.magnifyingGlass
|
||||
.foregroundColor(Color.appGrayTextContrast)
|
||||
}
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
if UIDevice.isIPhone {
|
||||
Menu(content: {
|
||||
Button(action: {
|
||||
isEditMode = isEditMode == .inactive ? .active : .inactive
|
||||
}, label: {
|
||||
Text(isEditMode == .inactive ? "Select Multiple" : "End Multiselect")
|
||||
})
|
||||
Button(action: { addLinkPresented = true }, label: {
|
||||
Label("Add Link", systemImage: "plus.circle")
|
||||
})
|
||||
Button(action: { settingsPresented = true }, label: {
|
||||
Label(LocalText.genericProfile, systemImage: "person.circle")
|
||||
})
|
||||
|
||||
}, label: {
|
||||
Image.utilityMenu
|
||||
})
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
Button(
|
||||
action: { isEditMode = isEditMode == .active ? .inactive : .active },
|
||||
label: {
|
||||
Image.selectMultiple
|
||||
.foregroundColor(Color.appGrayTextContrast)
|
||||
}
|
||||
)
|
||||
}
|
||||
ToolbarItem(placement: .barTrailing) {
|
||||
if viewModel.folder == "inbox" {
|
||||
Button(
|
||||
action: { addLinkPresented = true },
|
||||
label: {
|
||||
Image.addLink
|
||||
.foregroundColor(Color.appGrayTextContrast)
|
||||
}
|
||||
)
|
||||
} else {
|
||||
EmptyView()
|
||||
}
|
||||
}
|
||||
ToolbarItemGroup(placement: .bottomBar) {
|
||||
|
|
@ -368,7 +357,9 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
@Binding var isListScrolled: Bool
|
||||
@Binding var prefersListLayout: Bool
|
||||
@Binding var isEditMode: EditMode
|
||||
@State private var showAddFeedView = false
|
||||
@State private var showHideFeatureAlert = false
|
||||
@State private var showHideFollowingAlert = false
|
||||
|
||||
@Binding var selection: Set<String>
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
|
@ -379,23 +370,6 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
@State var topItem: Models.LibraryItem?
|
||||
@ObservedObject var networkMonitor = NetworkMonitor()
|
||||
|
||||
// init(listTitle: Binding<String>,
|
||||
// isListScrolled: Binding<Bool>,
|
||||
// prefersListLayout: Binding<Bool>,
|
||||
// isEditMode: Binding<EditMode>,
|
||||
// selection: Binding<Set<String>>,
|
||||
// viewModel: HomeFeedViewModel,
|
||||
// showFeatureCards: Bool)
|
||||
// {
|
||||
// self._listTitle = listTitle
|
||||
// self._isListScrolled = isListScrolled
|
||||
// self._prefersListLayout = prefersListLayout
|
||||
// self._isEditMode = isEditMode
|
||||
// self._selection = selection
|
||||
// self.viewModel = viewModel
|
||||
// self.showFeatureCards = showFeatureCards
|
||||
// }
|
||||
|
||||
var filtersHeader: some View {
|
||||
GeometryReader { reader in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
|
|
@ -603,8 +577,94 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
}
|
||||
|
||||
var redactedItems: some View {
|
||||
ForEach(Array(fakeLibraryItems(dataService: dataService).enumerated()), id: \.1.unwrappedID) { _, item in
|
||||
let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10)
|
||||
LibraryItemCard(item: item, viewer: dataService.currentViewer)
|
||||
.listRowSeparatorTint(Color.thBorderColor)
|
||||
.listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset))
|
||||
}.redacted(reason: .placeholder)
|
||||
}
|
||||
|
||||
var emptyState: some View {
|
||||
if viewModel.folder == "following" {
|
||||
return AnyView(
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("You don't have any Feed items.")
|
||||
.font(Font.system(size: 18, weight: .bold))
|
||||
|
||||
Text("Add an RSS/Atom feed")
|
||||
.foregroundColor(Color.blue)
|
||||
.onTapGesture {
|
||||
showAddFeedView = true
|
||||
}
|
||||
|
||||
Text("Hide the Following tab")
|
||||
.foregroundColor(Color.blue)
|
||||
.onTapGesture {
|
||||
showHideFollowingAlert = true
|
||||
}
|
||||
}
|
||||
.frame(minHeight: 400)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
)
|
||||
} else {
|
||||
return AnyView(Group {
|
||||
Spacer()
|
||||
|
||||
VStack(alignment: .center, spacing: 20) {
|
||||
Text("No results found for this query")
|
||||
.font(Font.system(size: 18, weight: .bold))
|
||||
}
|
||||
.frame(minHeight: 400)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
|
||||
Spacer()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
var listItems: some View {
|
||||
ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in
|
||||
let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10)
|
||||
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
isInMultiSelectMode: viewModel.isInMultiSelectMode,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.background(GeometryReader { geometry in
|
||||
Color.clear
|
||||
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
|
||||
})
|
||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
|
||||
if value.y < 100, value.y > 0 {
|
||||
if item.savedAt != nil, topItem != item {
|
||||
setTopItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowSeparatorTint(Color.thBorderColor)
|
||||
.listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset))
|
||||
.contextMenu {
|
||||
menuItems(for: item)
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in
|
||||
swipeActionButton(action: action, item: item)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in
|
||||
swipeActionButton(action: action, item: item)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
let horizontalInset = CGFloat(UIDevice.isIPad ? 20 : 10)
|
||||
VStack(spacing: 0) {
|
||||
Color.systemBackground.frame(height: 1)
|
||||
ScrollViewReader { reader in
|
||||
|
|
@ -643,38 +703,13 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
}
|
||||
}
|
||||
|
||||
ForEach(Array(viewModel.fetcher.items.enumerated()), id: \.1.unwrappedID) { _, item in
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
isInMultiSelectMode: viewModel.isInMultiSelectMode,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.background(GeometryReader { geometry in
|
||||
Color.clear
|
||||
.preference(key: ScrollOffsetPreferenceKey.self, value: geometry.frame(in: .named("scroll")).origin)
|
||||
})
|
||||
.onPreferenceChange(ScrollOffsetPreferenceKey.self) { value in
|
||||
if value.y < 100, value.y > 0 {
|
||||
if item.savedAt != nil, topItem != item {
|
||||
setTopItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listRowSeparatorTint(Color.thBorderColor)
|
||||
.listRowInsets(.init(top: 0, leading: horizontalInset, bottom: 10, trailing: horizontalInset))
|
||||
.contextMenu {
|
||||
menuItems(for: item)
|
||||
}
|
||||
.swipeActions(edge: .leading, allowsFullSwipe: true) {
|
||||
ForEach(viewModel.listConfig.leadingSwipeActions, id: \.self) { action in
|
||||
swipeActionButton(action: action, item: item)
|
||||
}
|
||||
}
|
||||
.swipeActions(edge: .trailing, allowsFullSwipe: true) {
|
||||
ForEach(viewModel.listConfig.trailingSwipeActions, id: \.self) { action in
|
||||
swipeActionButton(action: action, item: item)
|
||||
}
|
||||
}
|
||||
if viewModel.showLoadingBar {
|
||||
redactedItems
|
||||
} else if viewModel.fetcher.items.isEmpty {
|
||||
emptyState
|
||||
.listRowSeparator(.hidden, edges: .all)
|
||||
} else {
|
||||
listItems
|
||||
}
|
||||
}
|
||||
}, header: {
|
||||
|
|
@ -698,13 +733,26 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
shouldScrollToTop = true
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showAddFeedView) {
|
||||
NavigationView {
|
||||
LibraryAddFeedView()
|
||||
}
|
||||
}
|
||||
.alert("The Feature Section will be removed from your library. You can add it back from the filter settings in your profile.",
|
||||
isPresented: $showHideFeatureAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.hideFeatureSection = true
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFeatureAlert = false }
|
||||
}.introspectNavigationController { nav in
|
||||
}
|
||||
.alert("The Following tab will be hidden. You can add it back from the filter settings in your profile.",
|
||||
isPresented: $showHideFollowingAlert) {
|
||||
Button("OK", role: .destructive) {
|
||||
viewModel.hideFollowingTab = true
|
||||
}
|
||||
Button(LocalText.cancelGeneric, role: .cancel) { self.showHideFollowingAlert = false }
|
||||
}
|
||||
.introspectNavigationController { nav in
|
||||
nav.navigationBar.shadowImage = UIImage()
|
||||
nav.navigationBar.setBackgroundImage(UIImage(), for: .default)
|
||||
}
|
||||
|
|
@ -863,13 +911,24 @@ struct AnimatingCellHeight: AnimatableModifier {
|
|||
|
||||
ScrollView {
|
||||
LazyVGrid(columns: [GridItem(.adaptive(minimum: 325, maximum: 400), spacing: 16)], alignment: .center, spacing: 30) {
|
||||
ForEach(viewModel.fetcher.items) { item in
|
||||
GridCardNavigationLink(
|
||||
item: item,
|
||||
actionHandler: { contextMenuActionHandler(item: item, action: $0) },
|
||||
isContextMenuOpen: $isContextMenuOpen,
|
||||
viewModel: viewModel
|
||||
)
|
||||
if viewModel.showLoadingBar {
|
||||
ForEach(fakeLibraryItems(dataService: dataService)) { item in
|
||||
GridCardNavigationLink(
|
||||
item: item,
|
||||
actionHandler: { contextMenuActionHandler(item: item, action: $0) },
|
||||
isContextMenuOpen: $isContextMenuOpen,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}.redacted(reason: .placeholder)
|
||||
} else {
|
||||
ForEach(viewModel.fetcher.items) { item in
|
||||
GridCardNavigationLink(
|
||||
item: item,
|
||||
actionHandler: { contextMenuActionHandler(item: item, action: $0) },
|
||||
isContextMenuOpen: $isContextMenuOpen,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
Spacer()
|
||||
}
|
||||
|
|
@ -949,3 +1008,21 @@ struct LinkDestination: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func fakeLibraryItems(dataService: DataService) -> [Models.LibraryItem] {
|
||||
let temp = Models.LibraryItem(context: dataService.viewContext)
|
||||
temp.id = UUID().uuidString
|
||||
temp.wordsCount = 100
|
||||
temp.author = "the author"
|
||||
temp.siteName = "omnivore dot app"
|
||||
temp.title = "This is a temporary title for a fake item"
|
||||
temp.highlights = []
|
||||
temp.imageURLString = "https://localhost/"
|
||||
return Array(
|
||||
repeatElement(temp, count: 20)
|
||||
.map { item in
|
||||
item.id = UUID().uuidString
|
||||
return item
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,8 @@ import Views
|
|||
@AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false
|
||||
@AppStorage(UserDefaultKey.lastSelectedFeaturedItemFilter.rawValue) var featureFilter = FeaturedItemFilter.continueReading.rawValue
|
||||
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
|
||||
@Published var appliedFilter: InternalFilter? {
|
||||
didSet {
|
||||
let filterKey = UserDefaults.standard.string(forKey: "lastSelected-\(folder)-filter") ?? folder
|
||||
|
|
@ -152,10 +154,9 @@ import Views
|
|||
|
||||
func loadItems(dataService: DataService, isRefresh: Bool) async {
|
||||
isLoading = true
|
||||
showLoadingBar = true
|
||||
showLoadingBar = isRefresh
|
||||
|
||||
await fetcher.loadItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh)
|
||||
|
||||
updateFeatureFilter(context: dataService.viewContext, filter: FeaturedItemFilter(rawValue: featureFilter))
|
||||
|
||||
isLoading = false
|
||||
|
|
@ -164,12 +165,10 @@ import Views
|
|||
|
||||
func loadMoreItems(dataService: DataService, filterState: FetcherFilterState, isRefresh: Bool) async {
|
||||
isLoading = true
|
||||
showLoadingBar = true
|
||||
|
||||
await fetcher.loadMoreItems(dataService: dataService, filterState: filterState, isRefresh: isRefresh)
|
||||
|
||||
isLoading = false
|
||||
showLoadingBar = false
|
||||
}
|
||||
|
||||
func loadFeatureItems(context: NSManagedObjectContext, predicate: NSPredicate, sort: NSSortDescriptor) async -> [Models.LibraryItem] {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,156 @@
|
|||
|
||||
import Introspect
|
||||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Views
|
||||
|
||||
@MainActor final class LibraryAddFeedViewModel: NSObject, ObservableObject {
|
||||
@Published var isLoading = false
|
||||
@Published var errorMessage: String = ""
|
||||
@Published var showErrorMessage: Bool = false
|
||||
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
func addLink(dataService: DataService, newLinkURL: String, dismiss: DismissAction) {
|
||||
isLoading = true
|
||||
Task {
|
||||
if URL(string: newLinkURL) == nil {
|
||||
error("Invalid link")
|
||||
} else {
|
||||
let result = try? await dataService.saveURL(id: UUID().uuidString, url: newLinkURL)
|
||||
if result == nil {
|
||||
error("Error adding link")
|
||||
} else {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
func error(_ msg: String) {
|
||||
errorMessage = msg
|
||||
showErrorMessage = true
|
||||
isLoading = false
|
||||
}
|
||||
}
|
||||
|
||||
struct LibraryAddFeedView: View {
|
||||
@StateObject var viewModel = LibraryAddFeedViewModel()
|
||||
|
||||
@State var newLinkURL: String = ""
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
enum FocusField: Hashable {
|
||||
case addLinkEditor
|
||||
}
|
||||
|
||||
@FocusState private var focusedField: FocusField?
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
#if os(iOS)
|
||||
Form {
|
||||
innerBody
|
||||
.navigationTitle("Add Link")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
#else
|
||||
innerBody
|
||||
#endif
|
||||
}
|
||||
#if os(macOS)
|
||||
.padding()
|
||||
#endif
|
||||
.onAppear {
|
||||
focusedField = .addLinkEditor
|
||||
}
|
||||
.navigationTitle("Add Link")
|
||||
#if os(iOS)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
dismissButton
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
viewModel.isLoading ? AnyView(ProgressView()) : AnyView(addButton)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
.alert(viewModel.errorMessage,
|
||||
isPresented: $viewModel.showErrorMessage) {
|
||||
Button(LocalText.genericOk, role: .cancel) { viewModel.showErrorMessage = false }
|
||||
}
|
||||
}
|
||||
|
||||
var cancelButton: some View {
|
||||
Button(
|
||||
action: { dismiss() },
|
||||
label: { Text(LocalText.cancelGeneric).foregroundColor(.appGrayTextContrast) }
|
||||
)
|
||||
}
|
||||
|
||||
var pasteboardString: String? {
|
||||
#if os(iOS)
|
||||
UIPasteboard.general.url?.absoluteString
|
||||
#else
|
||||
NSPasteboard.general.string(forType: NSPasteboard.PasteboardType.URL)
|
||||
#endif
|
||||
}
|
||||
|
||||
var innerBody: some View {
|
||||
Group {
|
||||
TextField("Add Link", text: $newLinkURL)
|
||||
#if os(iOS)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
.autocorrectionDisabled(true)
|
||||
.textFieldStyle(StandardTextFieldStyle())
|
||||
.focused($focusedField, equals: .addLinkEditor)
|
||||
|
||||
Button(action: {
|
||||
if let url = pasteboardString {
|
||||
newLinkURL = url
|
||||
} else {
|
||||
viewModel.error("No URL on pasteboard")
|
||||
}
|
||||
}, label: {
|
||||
Text("Get from pasteboard")
|
||||
})
|
||||
|
||||
#if os(macOS)
|
||||
Spacer()
|
||||
HStack {
|
||||
cancelButton
|
||||
Spacer()
|
||||
addButton
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
var addButton: some View {
|
||||
Button(
|
||||
action: {
|
||||
viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss)
|
||||
},
|
||||
label: { Text("Add").bold() }
|
||||
)
|
||||
.keyboardShortcut(.defaultAction)
|
||||
.onSubmit {
|
||||
viewModel.addLink(dataService: dataService, newLinkURL: newLinkURL, dismiss: dismiss)
|
||||
}
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
|
||||
var dismissButton: some View {
|
||||
Button(
|
||||
action: { dismiss() },
|
||||
label: { Text(LocalText.genericClose) }
|
||||
)
|
||||
.disabled(viewModel.isLoading)
|
||||
}
|
||||
}
|
||||
|
|
@ -33,11 +33,11 @@
|
|||
performTypeahead(searchTerm)
|
||||
}
|
||||
|
||||
func performSearch(_: String) {
|
||||
// let term = searchTerm.trimmingCharacters(in: Foundation.CharacterSet.whitespacesAndNewlines)
|
||||
// viewModel.saveRecentSearch(dataService: dataService, searchTerm: term)
|
||||
// recents = viewModel.recentSearches(dataService: dataService)
|
||||
// homeFeedViewModel.searchTerm = term
|
||||
func performSearch(_ searchTerm: String) {
|
||||
let term = searchTerm.trimmingCharacters(in: Foundation.CharacterSet.whitespacesAndNewlines)
|
||||
viewModel.saveRecentSearch(dataService: dataService, searchTerm: term)
|
||||
recents = viewModel.recentSearches(dataService: dataService)
|
||||
homeFeedViewModel.searchTerm = term
|
||||
|
||||
dismiss()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ struct LibraryTabView: View {
|
|||
@EnvironmentObject var dataService: DataService
|
||||
@EnvironmentObject var audioController: AudioController
|
||||
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
@AppStorage(UserDefaultKey.lastSelectedTabItem.rawValue) var selectedTab = "inbox"
|
||||
|
||||
@State var showExpandedAudioPlayer = false
|
||||
|
|
@ -53,11 +54,13 @@ struct LibraryTabView: View {
|
|||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
TabView(selection: $selectedTab) {
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: followingViewModel)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(.stack)
|
||||
}.tag("following")
|
||||
if !hideFollowingTab {
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: followingViewModel)
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationViewStyle(.stack)
|
||||
}.tag("following")
|
||||
}
|
||||
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: libraryViewModel)
|
||||
|
|
@ -80,7 +83,7 @@ struct LibraryTabView: View {
|
|||
.frame(height: 1)
|
||||
.frame(maxWidth: .infinity)
|
||||
}
|
||||
CustomTabBar(selectedTab: $selectedTab)
|
||||
CustomTabBar(selectedTab: $selectedTab, hideFollowingTab: hideFollowingTab)
|
||||
.padding(0)
|
||||
}
|
||||
.fullScreenCover(isPresented: $showExpandedAudioPlayer) {
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import Views
|
|||
@Published var networkError = false
|
||||
@Published var libraryFilters = [InternalFilter]()
|
||||
|
||||
@AppStorage("LibraryTabView::hideFollowingTab") var hideFollowingTab = false
|
||||
@AppStorage(UserDefaultKey.hideFeatureSection.rawValue) var hideFeatureSection = false
|
||||
|
||||
func loadFilters(dataService: DataService) async {
|
||||
|
|
@ -50,7 +51,8 @@ struct FiltersView: View {
|
|||
private var innerBody: some View {
|
||||
List {
|
||||
Section {
|
||||
Toggle("Hide Feature Section", isOn: $viewModel.hideFeatureSection)
|
||||
Toggle("Hide following tab", isOn: $viewModel.hideFollowingTab)
|
||||
Toggle("Hide feature section", isOn: $viewModel.hideFeatureSection)
|
||||
}
|
||||
|
||||
Section(header: Text("Saved Searches")) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,13 @@ import SwiftUI
|
|||
|
||||
struct CustomTabBar: View {
|
||||
@Binding var selectedTab: String
|
||||
let hideFollowingTab: Bool
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 0) {
|
||||
TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab)
|
||||
if !hideFollowingTab {
|
||||
TabBarButton(key: "following", image: Image.tabFollowing, selectedTab: $selectedTab)
|
||||
}
|
||||
TabBarButton(key: "inbox", image: Image.tabLibrary, selectedTab: $selectedTab)
|
||||
TabBarButton(key: "profile", image: Image.tabProfile, selectedTab: $selectedTab)
|
||||
}
|
||||
|
|
|
|||
31
apple/OmnivoreKit/Sources/Utils/FirstFewWords.swift
Normal file
31
apple/OmnivoreKit/Sources/Utils/FirstFewWords.swift
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
//
|
||||
// File.swift
|
||||
//
|
||||
//
|
||||
// Created by Jackson Harper on 12/8/23.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import NaturalLanguage
|
||||
|
||||
public func extractFirstFewWords(_ title: String) -> String {
|
||||
let languageRecognizer = NLLanguageRecognizer()
|
||||
|
||||
languageRecognizer.processString(title)
|
||||
let language = languageRecognizer.dominantLanguage ?? NLLanguage.english
|
||||
|
||||
let tokenizer = NLTokenizer(unit: .word)
|
||||
tokenizer.setLanguage(language)
|
||||
tokenizer.string = title
|
||||
|
||||
var words: [String] = []
|
||||
tokenizer.enumerateTokens(in: title.startIndex ..< title.endIndex) { range, _ in
|
||||
let word = String(title[range])
|
||||
words.append(word)
|
||||
return true
|
||||
}
|
||||
|
||||
print("WORDS: ", words)
|
||||
let truncatedTitle = words.prefix(2).joined(separator: " ")
|
||||
return truncatedTitle
|
||||
}
|
||||
|
|
@ -44,6 +44,7 @@ public extension Color {
|
|||
static var themeSolidBackground: Color { Color("_themeSolidBackground", bundle: .module) }
|
||||
static var thBorderColor: Color { Color("thBorderColor", bundle: .module) }
|
||||
static var thLibrarySeparator: Color { Color("thLibrarySeparator", bundle: .module) }
|
||||
static var thLightWhiteGrey: Color { Color("_themeLightWhiteGrey", bundle: .module) }
|
||||
|
||||
static var thFeatureSeparator: Color { Color("featureSeparator", bundle: .module) }
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"colors" : [
|
||||
{
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xED",
|
||||
"green" : "0xED",
|
||||
"red" : "0xED"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
},
|
||||
{
|
||||
"appearances" : [
|
||||
{
|
||||
"appearance" : "luminosity",
|
||||
"value" : "dark"
|
||||
}
|
||||
],
|
||||
"color" : {
|
||||
"color-space" : "srgb",
|
||||
"components" : {
|
||||
"alpha" : "1.000",
|
||||
"blue" : "0xED",
|
||||
"green" : "0xED",
|
||||
"red" : "0xED"
|
||||
}
|
||||
},
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
|
|
@ -107,18 +107,26 @@ public struct GridCard: View {
|
|||
.cornerRadius(5)
|
||||
}
|
||||
|
||||
var fallbackFont: Font {
|
||||
if let uifont = UIFont(name: "Futura Bold", size: 16) {
|
||||
return Font(uifont)
|
||||
}
|
||||
return Font.system(size: 16)
|
||||
}
|
||||
|
||||
var fallbackImage: some View {
|
||||
GeometryReader { geo in
|
||||
HStack {
|
||||
Text(item.unwrappedTitle.prefix(1))
|
||||
.font(Font.system(size: 128, weight: .bold))
|
||||
.offset(CGSize(width: -48, height: 12))
|
||||
.frame(alignment: .bottomLeading)
|
||||
.foregroundColor(Gradient.randomColor(str: item.unwrappedTitle, offset: 1))
|
||||
Text(item.unwrappedTitle)
|
||||
.font(fallbackFont)
|
||||
.frame(alignment: .center)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(2)
|
||||
.padding(10)
|
||||
.foregroundColor(Color.themeMiddleGray)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Gradient.randomColor(str: item.unwrappedTitle, offset: 0))
|
||||
.background(LinearGradient(gradient: Gradient(fromStr: item.unwrappedTitle)!, startPoint: .top, endPoint: .bottom))
|
||||
.background(Color.thLightWhiteGrey)
|
||||
.frame(width: geo.size.width, height: geo.size.height)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,17 +66,25 @@ public struct LibraryFeatureCard: View {
|
|||
.cornerRadius(5)
|
||||
}
|
||||
|
||||
var fallbackFont: Font {
|
||||
if let uifont = UIFont(name: "Futura Bold", size: 16) {
|
||||
return Font(uifont)
|
||||
}
|
||||
return Font.system(size: 16)
|
||||
}
|
||||
|
||||
var fallbackImage: some View {
|
||||
HStack {
|
||||
Text(item.unwrappedTitle.prefix(1))
|
||||
.font(Font.system(size: 128, weight: .bold))
|
||||
.offset(CGSize(width: -48, height: 12))
|
||||
.frame(alignment: .bottomLeading)
|
||||
.foregroundColor(Gradient.randomColor(str: item.unwrappedTitle, offset: 1))
|
||||
Text(item.unwrappedTitle)
|
||||
.font(fallbackFont)
|
||||
.frame(alignment: .center)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(2)
|
||||
.padding(10)
|
||||
.foregroundColor(Color.themeMiddleGray)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.background(Gradient.randomColor(str: item.unwrappedTitle, offset: 0))
|
||||
.background(LinearGradient(gradient: Gradient(fromStr: item.unwrappedTitle)!, startPoint: .top, endPoint: .bottom))
|
||||
.background(Color.thLightWhiteGrey)
|
||||
.frame(width: 146, height: 90)
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -21,6 +21,10 @@ public extension Image {
|
|||
static var readerSettings: Image { Image("reader-settings", bundle: .module) }
|
||||
static var utilityMenu: Image { Image("utility-menu", bundle: .module) }
|
||||
|
||||
static var addLink: Image { Image("add-link", bundle: .module) }
|
||||
static var selectMultiple: Image { Image("select-multiple", bundle: .module) }
|
||||
static var magnifyingGlass: Image { Image("magnifying-glass", bundle: .module) }
|
||||
|
||||
static var archive: Image { Image("archive", bundle: .module) }
|
||||
static var unarchive: Image { Image("unarchive", bundle: .module) }
|
||||
static var remove: Image { Image("remove", bundle: .module) }
|
||||
|
|
|
|||
24
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/add-link.imageset/Contents.json
vendored
Normal file
24
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/add-link.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "add-link.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
11
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/add-link.imageset/add-link.svg
vendored
Normal file
11
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/add-link.imageset/add-link.svg
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="26" height="25" viewBox="0 0 26 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6802_32635)">
|
||||
<path d="M13.3398 5.20801V19.7913" stroke="#EDEDED" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.0481 12.5H20.6314" stroke="#EDEDED" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6802_32635">
|
||||
<rect width="25" height="25" fill="white" transform="translate(0.839844)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 519 B |
24
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/magnifying-glass.imageset/Contents.json
vendored
Normal file
24
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/magnifying-glass.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "magnifying-glass.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="26" height="25" viewBox="0 0 26 25" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6802_32619)">
|
||||
<path d="M5.08984 10.6667C5.08984 11.5093 5.25582 12.3437 5.57828 13.1222C5.90075 13.9007 6.3734 14.6081 6.96924 15.2039C7.56508 15.7998 8.27245 16.2724 9.05096 16.5949C9.82946 16.9174 10.6639 17.0833 11.5065 17.0833C12.3492 17.0833 13.1836 16.9174 13.9621 16.5949C14.7406 16.2724 15.4479 15.7998 16.0438 15.2039C16.6396 14.6081 17.1123 13.9007 17.4347 13.1222C17.7572 12.3437 17.9232 11.5093 17.9232 10.6667C17.9232 9.82402 17.7572 8.98962 17.4347 8.21111C17.1123 7.43261 16.6396 6.72524 16.0438 6.1294C15.4479 5.53356 14.7406 5.06091 13.9621 4.73844C13.1836 4.41597 12.3492 4.25 11.5065 4.25C10.6639 4.25 9.82946 4.41597 9.05096 4.73844C8.27245 5.06091 7.56508 5.53356 6.96924 6.1294C6.3734 6.72524 5.90075 7.43261 5.57828 8.21111C5.25582 8.98962 5.08984 9.82402 5.08984 10.6667Z" stroke="#EDEDED" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.5898 20.75L16.0898 15.25" stroke="#EDEDED" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6802_32619">
|
||||
<rect width="25" height="25" fill="white" transform="translate(0.839844)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
24
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/select-multiple.imageset/Contents.json
vendored
Normal file
24
apple/OmnivoreKit/Sources/Views/Images/Images.xcassets/select-multiple.imageset/Contents.json
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{
|
||||
"images" : [
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "1x"
|
||||
},
|
||||
{
|
||||
"filename" : "multiple-select.svg",
|
||||
"idiom" : "universal",
|
||||
"scale" : "2x"
|
||||
},
|
||||
{
|
||||
"idiom" : "universal",
|
||||
"scale" : "3x"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
},
|
||||
"properties" : {
|
||||
"template-rendering-intent" : "template"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
<svg width="26" height="26" viewBox="0 0 26 26" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_6802_32631)">
|
||||
<path d="M10.3398 12.2402L13.3398 15.2402L21.3398 7.24023" stroke="#EDEDED" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M21.3398 13.2402V19.2402C21.3398 19.7707 21.1291 20.2794 20.7541 20.6544C20.379 21.0295 19.8703 21.2402 19.3398 21.2402H7.33984C6.80941 21.2402 6.3007 21.0295 5.92563 20.6544C5.55056 20.2794 5.33984 19.7707 5.33984 19.2402V7.24023C5.33984 6.7098 5.55056 6.20109 5.92563 5.82602C6.3007 5.45095 6.80941 5.24023 7.33984 5.24023H16.3398" stroke="#EDEDED" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</g>
|
||||
<defs>
|
||||
<clipPath id="clip0_6802_32631">
|
||||
<rect width="25" height="25" fill="white" transform="translate(0.839844 0.740234)"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 864 B |
Loading…
Reference in a new issue