mirror of
https://github.com/omnivore-app/omnivore.git
synced 2026-03-11 08:54:26 +00:00
Merge pull request #175 from omnivore-app/feature/ipad-library-grid
iPad library grid
This commit is contained in:
commit
cdeaa1ffff
10 changed files with 479 additions and 130 deletions
|
|
@ -12,6 +12,7 @@ struct FeedCardNavigationLink: View {
|
|||
@Binding var selectedLinkItem: FeedItem?
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
var body: some View {
|
||||
NavigationLink(
|
||||
destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item)),
|
||||
|
|
@ -28,3 +29,43 @@ struct FeedCardNavigationLink: View {
|
|||
FeedCard(item: item)
|
||||
}
|
||||
}
|
||||
|
||||
struct GridCardNavigationLink: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
|
||||
@State private var scale = 1.0
|
||||
@State private var isActive = false
|
||||
|
||||
let item: FeedItem
|
||||
let searchQuery: String
|
||||
let actionHandler: (GridCardAction) -> Void
|
||||
|
||||
@Binding var selectedLinkItem: FeedItem?
|
||||
@Binding var isContextMenuOpen: Bool
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
NavigationLink(
|
||||
destination: LinkItemDetailView(viewModel: LinkItemDetailViewModel(item: item)),
|
||||
isActive: $isActive
|
||||
) {
|
||||
EmptyView()
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.itemAppeared(item: item, searchQuery: searchQuery, dataService: dataService)
|
||||
}
|
||||
GridCard(item: item, isContextMenuOpen: $isContextMenuOpen, actionHandler: actionHandler, tapAction: {
|
||||
withAnimation {
|
||||
scale = 0.95
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(150)) {
|
||||
scale = 1.0
|
||||
isActive = true
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.scaleEffect(scale)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,44 +0,0 @@
|
|||
import Models
|
||||
import Services
|
||||
import SwiftUI
|
||||
import Utils
|
||||
import Views
|
||||
|
||||
struct FeedItemContextMenuView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
|
||||
let item: FeedItem
|
||||
|
||||
@Binding var selectedLinkItem: FeedItem?
|
||||
@Binding var snoozePresented: Bool
|
||||
@Binding var itemToSnooze: FeedItem?
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
var body: some View {
|
||||
if !item.isArchived {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: true)
|
||||
if item == selectedLinkItem {
|
||||
selectedLinkItem = nil
|
||||
}
|
||||
}
|
||||
}, label: { Label("Archive", systemImage: "archivebox") })
|
||||
} else {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: false)
|
||||
}
|
||||
}, label: { Label("Unarchive", systemImage: "tray.and.arrow.down.fill") })
|
||||
}
|
||||
if FeatureFlag.enableSnooze {
|
||||
Button {
|
||||
itemToSnooze = item
|
||||
snoozePresented = true
|
||||
} label: {
|
||||
Label { Text("Snooze") } icon: { Image.moon }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -7,38 +7,26 @@ import Utils
|
|||
import Views
|
||||
|
||||
#if os(iOS)
|
||||
struct CompactHomeView: View {
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
HomeFeedContainerView(viewModel: viewModel)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
NavigationLink(
|
||||
destination: { ProfileView() },
|
||||
label: {
|
||||
Image.profile
|
||||
.resizable()
|
||||
.frame(width: 26, height: 26)
|
||||
.padding()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accentColor(.appGrayTextContrast)
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeFeedContainerView: View {
|
||||
let isCompact: Bool
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@State private var searchQuery = ""
|
||||
@State private var snoozePresented = false
|
||||
@State private var itemToSnooze: FeedItem?
|
||||
@State private var selectedLinkItem: FeedItem?
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
var body: some View {
|
||||
if #available(iOS 15.0, *) {
|
||||
HomeFeedView(searchQuery: $searchQuery, viewModel: viewModel)
|
||||
Group {
|
||||
if #available(iOS 15.0, *) {
|
||||
HomeFeedView(
|
||||
isCompact: isCompact,
|
||||
searchQuery: $searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
snoozePresented: $snoozePresented,
|
||||
itemToSnooze: $itemToSnooze,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.refreshable {
|
||||
viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true)
|
||||
}
|
||||
|
|
@ -61,28 +49,95 @@ import Views
|
|||
.onSubmit(of: .search) {
|
||||
viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true)
|
||||
}
|
||||
} else {
|
||||
HomeFeedView(searchQuery: $searchQuery, viewModel: viewModel).toolbar {
|
||||
ToolbarItem {
|
||||
Button(
|
||||
action: { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) },
|
||||
label: { Label("Refresh Feed", systemImage: "arrow.clockwise") }
|
||||
)
|
||||
} else {
|
||||
HomeFeedView(
|
||||
isCompact: isCompact,
|
||||
searchQuery: $searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
snoozePresented: $snoozePresented,
|
||||
itemToSnooze: $itemToSnooze,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
Button(
|
||||
action: { viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true) },
|
||||
label: { Label("Refresh Feed", systemImage: "arrow.clockwise") }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Home")
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
// Don't refresh the list if the user is currently reading an article
|
||||
if selectedLinkItem == nil {
|
||||
viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true)
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in
|
||||
if let feedItem = notification.userInfo?["feedItem"] as? FeedItem {
|
||||
viewModel.pushFeedItem(item: feedItem)
|
||||
self.selectedLinkItem = feedItem
|
||||
}
|
||||
}
|
||||
.formSheet(isPresented: $snoozePresented) {
|
||||
SnoozeView(snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze) {
|
||||
viewModel.snoozeUntil(
|
||||
dataService: dataService,
|
||||
linkId: $0.feedItemId,
|
||||
until: $0.snoozeUntilDate,
|
||||
successMessage: $0.successMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if viewModel.items.isEmpty {
|
||||
viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeFeedView: View {
|
||||
let isCompact: Bool
|
||||
@Binding var searchQuery: String
|
||||
@Binding var selectedLinkItem: FeedItem?
|
||||
@Binding var snoozePresented: Bool
|
||||
@Binding var itemToSnooze: FeedItem?
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
var body: some View {
|
||||
if isCompact {
|
||||
HomeFeedListView(
|
||||
searchQuery: $searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
snoozePresented: $snoozePresented,
|
||||
itemToSnooze: $itemToSnooze,
|
||||
viewModel: viewModel
|
||||
)
|
||||
} else {
|
||||
HomeFeedGridView(
|
||||
searchQuery: $searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
snoozePresented: $snoozePresented,
|
||||
itemToSnooze: $itemToSnooze,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeFeedListView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Binding var searchQuery: String
|
||||
@Binding var selectedLinkItem: FeedItem?
|
||||
@Binding var snoozePresented: Bool
|
||||
@Binding var itemToSnooze: FeedItem?
|
||||
|
||||
@State private var selectedLinkItem: FeedItem?
|
||||
@State private var itemToRemove: FeedItem?
|
||||
@State private var confirmationShown = false
|
||||
@State private var snoozePresented = false
|
||||
@State private var itemToSnooze: FeedItem?
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
|
|
@ -90,21 +145,38 @@ import Views
|
|||
List {
|
||||
Section {
|
||||
ForEach(viewModel.items) { item in
|
||||
let link = ZStack {
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
searchQuery: searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}.contextMenu {
|
||||
FeedItemContextMenuView(
|
||||
item: item,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
snoozePresented: $snoozePresented,
|
||||
itemToSnooze: $itemToSnooze,
|
||||
viewModel: viewModel
|
||||
let link = FeedCardNavigationLink(
|
||||
item: item,
|
||||
searchQuery: searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
viewModel: viewModel
|
||||
)
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
withAnimation(.linear(duration: 0.4)) {
|
||||
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: !item.isArchived)
|
||||
}
|
||||
}, label: {
|
||||
Label(
|
||||
item.isArchived ? "Unarchive" : "Archive",
|
||||
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
|
||||
)
|
||||
})
|
||||
Button(
|
||||
action: {
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
},
|
||||
label: { Label("Delete Link", systemImage: "trash") }
|
||||
)
|
||||
if FeatureFlag.enableSnooze {
|
||||
Button {
|
||||
itemToSnooze = item
|
||||
snoozePresented = true
|
||||
} label: {
|
||||
Label { Text("Snooze") } icon: { Image.moon }
|
||||
}
|
||||
}
|
||||
}
|
||||
if #available(iOS 15.0, *) {
|
||||
link
|
||||
|
|
@ -170,38 +242,71 @@ import Views
|
|||
}
|
||||
}
|
||||
.listStyle(PlainListStyle())
|
||||
.navigationTitle("Home")
|
||||
.onReceive(NotificationCenter.default.publisher(for: UIApplication.willEnterForegroundNotification)) { _ in
|
||||
// Don't refresh the list if the user is currently reading an article
|
||||
if selectedLinkItem == nil {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
.onReceive(NotificationCenter.default.publisher(for: Notification.Name("PushFeedItem"))) { notification in
|
||||
if let feedItem = notification.userInfo?["feedItem"] as? FeedItem {
|
||||
viewModel.pushFeedItem(item: feedItem)
|
||||
self.selectedLinkItem = feedItem
|
||||
}
|
||||
}
|
||||
.formSheet(isPresented: $snoozePresented) {
|
||||
SnoozeView(snoozePresented: $snoozePresented, itemToSnooze: $itemToSnooze) {
|
||||
viewModel.snoozeUntil(
|
||||
dataService: dataService,
|
||||
linkId: $0.feedItemId,
|
||||
until: $0.snoozeUntilDate,
|
||||
successMessage: $0.successMessage
|
||||
)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if viewModel.items.isEmpty {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeFeedGridView: View {
|
||||
@EnvironmentObject var dataService: DataService
|
||||
@Binding var searchQuery: String
|
||||
@Binding var selectedLinkItem: FeedItem?
|
||||
@Binding var snoozePresented: Bool
|
||||
@Binding var itemToSnooze: FeedItem?
|
||||
|
||||
@State private var itemToRemove: FeedItem?
|
||||
@State private var confirmationShown = false
|
||||
@State var isContextMenuOpen = false
|
||||
|
||||
@ObservedObject var viewModel: HomeFeedViewModel
|
||||
|
||||
private let columns = Array(repeating: GridItem(.flexible(), spacing: 20), count: 2)
|
||||
|
||||
func contextMenuActionHandler(item: FeedItem, action: GridCardAction) {
|
||||
switch action {
|
||||
case .toggleArchiveStatus:
|
||||
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: !item.isArchived)
|
||||
case .delete:
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
}
|
||||
}
|
||||
|
||||
private func refresh() {
|
||||
viewModel.loadItems(dataService: dataService, searchQuery: searchQuery, isRefresh: true)
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
LazyVGrid(columns: columns, spacing: 20) {
|
||||
ForEach(viewModel.items) { item in
|
||||
let link = GridCardNavigationLink(
|
||||
item: item,
|
||||
searchQuery: searchQuery,
|
||||
actionHandler: { contextMenuActionHandler(item: item, action: $0) },
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
isContextMenuOpen: $isContextMenuOpen,
|
||||
viewModel: viewModel
|
||||
)
|
||||
if #available(iOS 15.0, *) {
|
||||
link
|
||||
.alert("Are you sure?", isPresented: $confirmationShown) {
|
||||
Button("Remove Link", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
withAnimation {
|
||||
viewModel.removeLink(dataService: dataService, linkId: itemToRemove.id)
|
||||
}
|
||||
}
|
||||
self.itemToRemove = nil
|
||||
}
|
||||
Button("Cancel", role: .cancel) { self.itemToRemove = nil }
|
||||
}
|
||||
} else {
|
||||
link
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
.background(Color(.systemGroupedBackground))
|
||||
|
||||
if viewModel.isLoading {
|
||||
LoadingSection()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -22,19 +22,54 @@ import Views
|
|||
List {
|
||||
Section {
|
||||
ForEach(viewModel.items) { item in
|
||||
ZStack {
|
||||
if #available(macOS 12.0, *) {
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
searchQuery: searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}.contextMenu {
|
||||
FeedItemContextMenuView(
|
||||
.contextMenu {
|
||||
Button(action: {
|
||||
viewModel.setLinkArchived(dataService: dataService, linkId: item.id, archived: !item.isArchived)
|
||||
}, label: {
|
||||
Label(
|
||||
item.isArchived ? "Unarchive" : "Archive",
|
||||
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
|
||||
)
|
||||
})
|
||||
Button(
|
||||
action: {
|
||||
itemToRemove = item
|
||||
confirmationShown = true
|
||||
},
|
||||
label: { Label("Delete Link", systemImage: "trash") }
|
||||
)
|
||||
if FeatureFlag.enableSnooze {
|
||||
Button {
|
||||
itemToSnooze = item
|
||||
snoozePresented = true
|
||||
} label: {
|
||||
Label { Text("Snooze") } icon: { Image.moon }
|
||||
}
|
||||
}
|
||||
}
|
||||
.alert("Are you sure?", isPresented: $confirmationShown) {
|
||||
Button("Remove Link", role: .destructive) {
|
||||
if let itemToRemove = itemToRemove {
|
||||
withAnimation {
|
||||
viewModel.removeLink(dataService: dataService, linkId: itemToRemove.id)
|
||||
self.itemToRemove = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
Button("Cancel", role: .cancel) { self.itemToRemove = nil }
|
||||
}
|
||||
} else {
|
||||
FeedCardNavigationLink(
|
||||
item: item,
|
||||
searchQuery: searchQuery,
|
||||
selectedLinkItem: $selectedLinkItem,
|
||||
snoozePresented: $snoozePresented,
|
||||
itemToSnooze: $itemToSnooze,
|
||||
viewModel: viewModel
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,9 +6,27 @@ struct HomeView: View {
|
|||
var body: some View {
|
||||
#if os(iOS)
|
||||
if UIDevice.isIPhone {
|
||||
CompactHomeView(viewModel: viewModel)
|
||||
NavigationView {
|
||||
HomeFeedContainerView(isCompact: true, viewModel: viewModel)
|
||||
.toolbar {
|
||||
ToolbarItem {
|
||||
NavigationLink(
|
||||
destination: { ProfileView() },
|
||||
label: {
|
||||
Image.profile
|
||||
.resizable()
|
||||
.frame(width: 26, height: 26)
|
||||
.padding()
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.accentColor(.appGrayTextContrast)
|
||||
} else {
|
||||
HomeFeedContainerView(viewModel: viewModel)
|
||||
GeometryReader { geo in
|
||||
HomeFeedContainerView(isCompact: geo.size.width < 500, viewModel: viewModel)
|
||||
}
|
||||
}
|
||||
#elseif os(macOS)
|
||||
HomeFeedView(viewModel: viewModel)
|
||||
|
|
|
|||
|
|
@ -14,4 +14,5 @@ public enum FeatureFlag {
|
|||
public static let enablePushNotifications = false
|
||||
public static let enableShareButton = false
|
||||
public static let enableSnooze = false
|
||||
public static let showFeedItemTags = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,8 +21,12 @@ public extension Color {
|
|||
#if os(iOS)
|
||||
static var systemBackground: Color { Color(.systemBackground) }
|
||||
static var systemPlaceholder: Color { Color(.placeholderText) }
|
||||
static var secondarySystemGroupedBackground: Color { Color(.secondarySystemGroupedBackground) }
|
||||
#elseif os(macOS)
|
||||
static var systemBackground: Color { Color(.windowBackgroundColor) }
|
||||
static var systemPlaceholder: Color { Color(.placeholderTextColor) }
|
||||
|
||||
// Just for compilation. secondarySystemGroupedBackground shouldn't be used on macOS
|
||||
static var secondarySystemGroupedBackground: Color { Color(.windowBackgroundColor) }
|
||||
#endif
|
||||
}
|
||||
|
|
|
|||
166
apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift
Normal file
166
apple/OmnivoreKit/Sources/Views/FeedItem/GridCard.swift
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
import Models
|
||||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
public enum GridCardAction {
|
||||
case toggleArchiveStatus
|
||||
case delete
|
||||
}
|
||||
|
||||
public struct GridCard: View {
|
||||
@Binding var isContextMenuOpen: Bool
|
||||
let item: FeedItem
|
||||
let actionHandler: (GridCardAction) -> Void
|
||||
let tapAction: () -> Void
|
||||
|
||||
public init(
|
||||
item: FeedItem,
|
||||
isContextMenuOpen: Binding<Bool>,
|
||||
actionHandler: @escaping (GridCardAction) -> Void,
|
||||
tapAction: @escaping () -> Void
|
||||
) {
|
||||
self.item = item
|
||||
self._isContextMenuOpen = isContextMenuOpen
|
||||
self.actionHandler = actionHandler
|
||||
self.tapAction = tapAction
|
||||
}
|
||||
|
||||
// Menu doesn't provide an API to observe it's open state
|
||||
// so we have keep track of it's state manually
|
||||
func tapHandler() {
|
||||
if isContextMenuOpen {
|
||||
isContextMenuOpen = false
|
||||
} else {
|
||||
tapAction()
|
||||
}
|
||||
}
|
||||
|
||||
func menuActionHandler(_ action: GridCardAction) {
|
||||
isContextMenuOpen = false
|
||||
actionHandler(action)
|
||||
}
|
||||
|
||||
var contextMenuView: some View {
|
||||
Group {
|
||||
Button(
|
||||
action: { menuActionHandler(.toggleArchiveStatus) },
|
||||
label: {
|
||||
Label(
|
||||
item.isArchived ? "Unarchive" : "Archive",
|
||||
systemImage: item.isArchived ? "tray.and.arrow.down.fill" : "archivebox"
|
||||
)
|
||||
}
|
||||
)
|
||||
Button(
|
||||
action: { menuActionHandler(.delete) },
|
||||
label: { Label("Delete Link", systemImage: "trash") }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
public var body: some View {
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
// Progress Bar
|
||||
Group {
|
||||
// Remove os check when dropping macOS 11
|
||||
#if os(iOS)
|
||||
if #available(iOS 15.0, *) {
|
||||
ProgressView(value: min(abs(item.readingProgress) / 100, 1))
|
||||
.tint(.appYellow48)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
} else {
|
||||
ProgressView(value: max(abs(item.readingProgress) / 100, 1))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
#else
|
||||
ProgressView(value: max(abs(item.readingProgress) / 100, 1))
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
#endif
|
||||
}
|
||||
.onTapGesture { tapHandler() }
|
||||
|
||||
// Title, Subtitle, Menu Button
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
HStack {
|
||||
Text(item.title)
|
||||
.font(.appHeadline)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.lineLimit(1)
|
||||
.onTapGesture { tapHandler() }
|
||||
Spacer()
|
||||
|
||||
Menu(
|
||||
content: { contextMenuView },
|
||||
label: { Image.profile }
|
||||
)
|
||||
.onTapGesture { isContextMenuOpen = true }
|
||||
}
|
||||
|
||||
HStack {
|
||||
if let author = item.author {
|
||||
Text("by \(author)")
|
||||
.font(.appCaptionTwo)
|
||||
.foregroundColor(.appGrayText)
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
if let publisherURL = item.publisherHostname {
|
||||
Text(publisherURL)
|
||||
.font(.appCaptionTwo)
|
||||
.foregroundColor(.appGrayText)
|
||||
.underline()
|
||||
.lineLimit(1)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.onTapGesture { tapHandler() }
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding(.horizontal)
|
||||
|
||||
// Link description and image
|
||||
HStack(alignment: .top) {
|
||||
Text(item.description ?? item.title)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(.appGrayTextContrast)
|
||||
.lineLimit(nil)
|
||||
.multilineTextAlignment(.leading)
|
||||
|
||||
Spacer()
|
||||
|
||||
if let imageURL = item.imageURL {
|
||||
AsyncImage(url: imageURL, isResizable: true)
|
||||
.aspectRatio(1, contentMode: .fill)
|
||||
.frame(width: 135, height: 90)
|
||||
.cornerRadius(3)
|
||||
}
|
||||
}
|
||||
.frame(height: 95)
|
||||
.padding(.horizontal)
|
||||
.onTapGesture { tapHandler() }
|
||||
|
||||
// Category Labels
|
||||
if FeatureFlag.showFeedItemTags {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack {
|
||||
TextChip(text: "label", color: .red)
|
||||
TextChip(text: "longer label", color: .blue)
|
||||
Spacer()
|
||||
}
|
||||
.frame(height: 30)
|
||||
.padding(.horizontal)
|
||||
.padding(.bottom, 8)
|
||||
}
|
||||
} else {
|
||||
Spacer(minLength: 8)
|
||||
}
|
||||
}
|
||||
.background(
|
||||
Color.secondarySystemGroupedBackground
|
||||
.onTapGesture { tapHandler() }
|
||||
)
|
||||
.cornerRadius(6)
|
||||
.contextMenu { contextMenuView }
|
||||
}
|
||||
}
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
import Models
|
||||
import SwiftUI
|
||||
import Utils
|
||||
|
||||
public struct FeedCard: View {
|
||||
let item: FeedItem
|
||||
22
apple/OmnivoreKit/Sources/Views/TextChip.swift
Normal file
22
apple/OmnivoreKit/Sources/Views/TextChip.swift
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import SwiftUI
|
||||
|
||||
struct TextChip: View {
|
||||
let text: String
|
||||
let color: Color
|
||||
let cornerRadius = 20.0
|
||||
|
||||
var body: some View {
|
||||
Text(text)
|
||||
.padding(.horizontal, 10)
|
||||
.padding(.vertical, 5)
|
||||
.font(.appFootnote)
|
||||
.foregroundColor(color)
|
||||
.lineLimit(1)
|
||||
.background(color.opacity(0.1))
|
||||
.cornerRadius(cornerRadius)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.stroke(color.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue