mirror of
https://github.com/yattee/yattee.git
synced 2025-08-09 20:24:06 +00:00
refactor Search
- have a separate body view for each os - macOS: set navigation title for search - macOS: set min width to 835 for main content - macOS: set main window min height to 600 - macOS: don’t have text behind the cancel button - split SearchTextField into macOS and iOS/tvOS - iOS: move search menu to the right - iOS: unified search field - make min width a constant - add option to disable search suggestions Signed-off-by: Toni Förster <toni.foerster@gmail.com>
This commit is contained in:
@@ -1,64 +1,95 @@
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
struct SearchTextField: View {
|
||||
private var navigation = NavigationModel.shared
|
||||
@ObservedObject private var state = SearchModel.shared
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
#if os(macOS)
|
||||
#if os(macOS)
|
||||
var body: some View {
|
||||
ZStack {
|
||||
fieldBorder
|
||||
#endif
|
||||
|
||||
HStack(spacing: 0) {
|
||||
#if os(macOS)
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 12, height: 12)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.horizontal, 6)
|
||||
.opacity(0.8)
|
||||
#endif
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
RecentsModel.shared.addQuery(state.queryText)
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
#if os(macOS)
|
||||
.frame(maxWidth: 190)
|
||||
.textFieldStyle(.plain)
|
||||
#else
|
||||
.frame(minWidth: 200)
|
||||
.textFieldStyle(.roundedBorder)
|
||||
.padding(.horizontal, 5)
|
||||
.padding(.trailing, state.queryText.isEmpty ? 0 : 10)
|
||||
#endif
|
||||
|
||||
if !state.queryText.isEmpty {
|
||||
clearButton
|
||||
} else {
|
||||
#if os(macOS)
|
||||
GeometryReader { geometry in
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
RecentsModel.shared.addQuery(state.queryText)
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
.frame(maxWidth: geometry.size.width - 5)
|
||||
.textFieldStyle(.plain)
|
||||
.padding(.vertical, 8)
|
||||
.frame(height: 27, alignment: .center)
|
||||
}
|
||||
|
||||
if !state.queryText.isEmpty {
|
||||
clearButton
|
||||
} else {
|
||||
clearButton
|
||||
.opacity(0)
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
.transaction { t in t.animation = nil }
|
||||
}
|
||||
.transaction { t in t.animation = nil }
|
||||
}
|
||||
#else
|
||||
var body: some View {
|
||||
ZStack {
|
||||
HStack {
|
||||
HStack(spacing: 0) {
|
||||
Image(systemName: "magnifyingglass")
|
||||
.foregroundColor(.gray)
|
||||
.padding(.leading, 5)
|
||||
.padding(.trailing, 5)
|
||||
.imageScale(.medium)
|
||||
|
||||
TextField("Search...", text: $state.queryText) {
|
||||
state.changeQuery { query in
|
||||
query.query = state.queryText
|
||||
navigation.hideKeyboard()
|
||||
}
|
||||
RecentsModel.shared.addQuery(state.queryText)
|
||||
}
|
||||
.disableAutocorrection(true)
|
||||
.textFieldStyle(.plain)
|
||||
.padding(.vertical, 7)
|
||||
|
||||
if !state.queryText.isEmpty {
|
||||
clearButton
|
||||
.padding(.leading, 5)
|
||||
.padding(.trailing, 5)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 8)
|
||||
.fill(Color("SearchTextFieldBackground"))
|
||||
)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
.padding(.horizontal, 0)
|
||||
}
|
||||
.transaction { t in t.animation = nil }
|
||||
}
|
||||
#endif
|
||||
|
||||
private var fieldBorder: some View {
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.fill(Color.background)
|
||||
.frame(width: 250, height: 32)
|
||||
.frame(width: 250, height: 27)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 5, style: .continuous)
|
||||
.stroke(Color.gray.opacity(0.4), lineWidth: 1)
|
||||
.frame(width: 250, height: 31)
|
||||
.frame(width: 250, height: 27)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -67,15 +98,14 @@ struct SearchTextField: View {
|
||||
self.state.queryText = ""
|
||||
}) {
|
||||
Image(systemName: "xmark.circle.fill")
|
||||
#if os(macOS)
|
||||
.imageScale(.small)
|
||||
#else
|
||||
.imageScale(.medium)
|
||||
#endif
|
||||
}
|
||||
.buttonStyle(PlainButtonStyle())
|
||||
#if os(macOS)
|
||||
.padding(.trailing, 10)
|
||||
.padding(.trailing, 5)
|
||||
#elseif os(iOS)
|
||||
.padding(.trailing, 5)
|
||||
.foregroundColor(.gray)
|
||||
#endif
|
||||
.opacity(0.7)
|
||||
}
|
||||
|
@@ -30,6 +30,7 @@ struct SearchView: View {
|
||||
@Default(.saveRecents) private var saveRecents
|
||||
@Default(.showHome) private var showHome
|
||||
@Default(.searchListingStyle) private var searchListingStyle
|
||||
@Default(.showSearchSuggestions) private var showSearchSuggestions
|
||||
|
||||
private var videos = [Video]()
|
||||
|
||||
@@ -38,9 +39,9 @@ struct SearchView: View {
|
||||
self.videos = videos
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
#if os(iOS)
|
||||
#if os(iOS)
|
||||
var body: some View {
|
||||
VStack {
|
||||
VStack {
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
|
||||
SearchSuggestions()
|
||||
@@ -51,27 +52,155 @@ struct SearchView: View {
|
||||
}
|
||||
.backport
|
||||
.scrollDismissesKeyboardInteractively()
|
||||
#else
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .principal) {
|
||||
if #available(iOS 15, *) {
|
||||
FocusableSearchTextField()
|
||||
} else {
|
||||
SearchTextField()
|
||||
}
|
||||
}
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
searchMenu
|
||||
}
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.navigationTitle("Search")
|
||||
.onAppear {
|
||||
if let query {
|
||||
state.queryText = query.query
|
||||
state.resetQuery(query)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
if !videos.isEmpty {
|
||||
state.store.replace(ContentItem.array(of: videos))
|
||||
}
|
||||
}
|
||||
.onChange(of: accounts.current) { _ in
|
||||
state.reloadQuery()
|
||||
}
|
||||
.onChange(of: state.queryText) { newQuery in
|
||||
if newQuery.isEmpty {
|
||||
favoriteItem = nil
|
||||
state.resetQuery()
|
||||
} else {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
state.loadSuggestions(newQuery)
|
||||
}
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
state.changeQuery { query in
|
||||
query.sortBy = order
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDate) { date in
|
||||
state.changeQuery { query in
|
||||
query.date = date
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDuration) { duration in
|
||||
state.changeQuery { query in
|
||||
query.duration = duration
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#elseif os(tvOS)
|
||||
var body: some View {
|
||||
VStack {
|
||||
ZStack {
|
||||
results
|
||||
|
||||
#if os(macOS)
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 280)
|
||||
.opacity(state.queryText.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.onAppear {
|
||||
if let query {
|
||||
state.queryText = query.query
|
||||
state.resetQuery(query)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
if !videos.isEmpty {
|
||||
state.store.replace(ContentItem.array(of: videos))
|
||||
}
|
||||
}
|
||||
.onChange(of: accounts.current) { _ in
|
||||
state.reloadQuery()
|
||||
}
|
||||
.onChange(of: state.queryText) { newQuery in
|
||||
if newQuery.isEmpty {
|
||||
favoriteItem = nil
|
||||
state.resetQuery()
|
||||
} else {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
if showSearchSuggestions {
|
||||
state.loadSuggestions(newQuery)
|
||||
}
|
||||
searchDebounce.invalidate()
|
||||
recentsDebounce.invalidate()
|
||||
|
||||
searchDebounce.debouncing(2) {
|
||||
state.changeQuery { query in
|
||||
query.query = newQuery
|
||||
}
|
||||
}
|
||||
|
||||
recentsDebounce.debouncing(10) {
|
||||
recents.addQuery(newQuery)
|
||||
}
|
||||
}
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
state.changeQuery { query in
|
||||
query.sortBy = order
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDate) { date in
|
||||
state.changeQuery { query in
|
||||
query.date = date
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDuration) { duration in
|
||||
state.changeQuery { query in
|
||||
query.duration = duration
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.searchable(text: $state.queryText) {
|
||||
if !state.queryText.isEmpty {
|
||||
ForEach(state.querySuggestions, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.toolbar {
|
||||
#if os(macOS)
|
||||
|
||||
#elseif os(macOS)
|
||||
var body: some View {
|
||||
ZStack {
|
||||
results
|
||||
if accounts.app.supportsSearchSuggestions, state.query.query != state.queryText, showSearchSuggestions {
|
||||
HStack {
|
||||
Spacer()
|
||||
SearchSuggestions()
|
||||
.borderLeading(width: 1, color: Color("ControlsBorderColor"))
|
||||
.frame(maxWidth: 262)
|
||||
.opacity(state.queryText.isEmpty ? 0 : 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
.environment(\.listingStyle, searchListingStyle)
|
||||
.toolbar {
|
||||
ToolbarItemGroup(placement: toolbarPlacement) {
|
||||
ListingStyleButtons(listingStyle: $searchListingStyle)
|
||||
HideWatchedButtons()
|
||||
@@ -84,7 +213,6 @@ struct SearchView: View {
|
||||
HStack {
|
||||
Text("Sort:")
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
searchSortOrderPicker
|
||||
}
|
||||
}
|
||||
@@ -101,94 +229,52 @@ struct SearchView: View {
|
||||
SearchTextField()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onAppear {
|
||||
if let query {
|
||||
state.queryText = query.query
|
||||
state.resetQuery(query)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
if !videos.isEmpty {
|
||||
state.store.replace(ContentItem.array(of: videos))
|
||||
}
|
||||
}
|
||||
.onChange(of: accounts.current) { _ in
|
||||
state.reloadQuery()
|
||||
}
|
||||
.onChange(of: state.queryText) { newQuery in
|
||||
if newQuery.isEmpty {
|
||||
favoriteItem = nil
|
||||
state.resetQuery()
|
||||
} else {
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
state.loadSuggestions(newQuery)
|
||||
|
||||
#if os(tvOS)
|
||||
searchDebounce.invalidate()
|
||||
recentsDebounce.invalidate()
|
||||
|
||||
searchDebounce.debouncing(2) {
|
||||
state.changeQuery { query in
|
||||
query.query = newQuery
|
||||
}
|
||||
.onAppear {
|
||||
if let query {
|
||||
state.queryText = query.query
|
||||
state.resetQuery(query)
|
||||
updateFavoriteItem()
|
||||
}
|
||||
|
||||
recentsDebounce.debouncing(10) {
|
||||
recents.addQuery(newQuery)
|
||||
}
|
||||
#endif
|
||||
}
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
state.changeQuery { query in
|
||||
query.sortBy = order
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDate) { date in
|
||||
state.changeQuery { query in
|
||||
query.date = date
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDuration) { duration in
|
||||
state.changeQuery { query in
|
||||
query.duration = duration
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
#if os(tvOS)
|
||||
.searchable(text: $state.queryText) {
|
||||
if !state.queryText.isEmpty {
|
||||
ForEach(state.querySuggestions, id: \.self) { suggestion in
|
||||
Text(suggestion)
|
||||
.searchCompletion(suggestion)
|
||||
if !videos.isEmpty {
|
||||
state.store.replace(ContentItem.array(of: videos))
|
||||
}
|
||||
}
|
||||
}
|
||||
#else
|
||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||
.navigationTitle("Search")
|
||||
#endif
|
||||
#if os(iOS)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarLeading) {
|
||||
searchMenu
|
||||
.onChange(of: accounts.current) { _ in
|
||||
state.reloadQuery()
|
||||
}
|
||||
ToolbarItem(placement: .principal) {
|
||||
if #available(iOS 15, *) {
|
||||
FocusableSearchTextField()
|
||||
.onChange(of: state.queryText) { newQuery in
|
||||
if newQuery.isEmpty {
|
||||
favoriteItem = nil
|
||||
state.resetQuery()
|
||||
} else {
|
||||
SearchTextField()
|
||||
updateFavoriteItem()
|
||||
}
|
||||
state.loadSuggestions(newQuery)
|
||||
}
|
||||
.onChange(of: searchSortOrder) { order in
|
||||
state.changeQuery { query in
|
||||
query.sortBy = order
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDate) { date in
|
||||
state.changeQuery { query in
|
||||
query.date = date
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.onChange(of: searchDuration) { duration in
|
||||
state.changeQuery { query in
|
||||
query.duration = duration
|
||||
updateFavoriteItem()
|
||||
}
|
||||
}
|
||||
.frame(minWidth: Constants.contentViewMinWidth)
|
||||
.navigationTitle("Search")
|
||||
}
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
var searchMenu: some View {
|
||||
@@ -230,11 +316,10 @@ struct SearchView: View {
|
||||
}
|
||||
} label: {
|
||||
HStack {
|
||||
Image(systemName: "magnifyingglass")
|
||||
Image(systemName: "chevron.down.circle.fill")
|
||||
.foregroundColor(.accentColor)
|
||||
.imageScale(.large)
|
||||
}
|
||||
.foregroundColor(.accentColor)
|
||||
.imageScale(.medium)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
Reference in New Issue
Block a user