mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-09-27 22:11:56 +00:00
News trending links experience
This commit is contained in:
parent
35d249f7c9
commit
e7bc857231
17 changed files with 214 additions and 61 deletions
|
@ -60,7 +60,7 @@ extension View {
|
||||||
scrollToTopSignal: .constant(0),
|
scrollToTopSignal: .constant(0),
|
||||||
canFilterTimeline: false)
|
canFilterTimeline: false)
|
||||||
case let .trendingLinks(cards):
|
case let .trendingLinks(cards):
|
||||||
CardsListView(cards: cards)
|
TrendingLinksListView(cards: cards)
|
||||||
case let .tagsList(tags):
|
case let .tagsList(tags):
|
||||||
TagsListView(tags: tags)
|
TagsListView(tags: tags)
|
||||||
}
|
}
|
||||||
|
@ -82,6 +82,9 @@ extension View {
|
||||||
case let .quoteStatusEditor(status):
|
case let .quoteStatusEditor(status):
|
||||||
StatusEditor.MainView(mode: .quote(status: status))
|
StatusEditor.MainView(mode: .quote(status: status))
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
|
case let .quoteLinkStatusEditor(link):
|
||||||
|
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||||
|
.withEnvironments()
|
||||||
case let .mentionStatusEditor(account, visibility):
|
case let .mentionStatusEditor(account, visibility):
|
||||||
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||||
.withEnvironments()
|
.withEnvironments()
|
||||||
|
|
|
@ -83,6 +83,8 @@ extension IceCubesApp {
|
||||||
StatusEditor.MainView(mode: .replyTo(status: status))
|
StatusEditor.MainView(mode: .replyTo(status: status))
|
||||||
case let .mentionStatusEditor(account, visibility):
|
case let .mentionStatusEditor(account, visibility):
|
||||||
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
StatusEditor.MainView(mode: .mention(account: account, visibility: visibility))
|
||||||
|
case let .quoteLinkStatusEditor(link):
|
||||||
|
StatusEditor.MainView(mode: .quoteLink(link: link))
|
||||||
case .none:
|
case .none:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
case post
|
case post
|
||||||
case followedTags
|
case followedTags
|
||||||
case lists
|
case lists
|
||||||
|
case links
|
||||||
|
|
||||||
nonisolated var id: Int {
|
nonisolated var id: Int {
|
||||||
rawValue
|
rawValue
|
||||||
|
@ -67,6 +68,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
NavigationTab {
|
NavigationTab {
|
||||||
ListsListView()
|
ListsListView()
|
||||||
}
|
}
|
||||||
|
case .links:
|
||||||
|
NavigationTab { TrendingLinksListView(cards: []) }
|
||||||
case .post:
|
case .post:
|
||||||
VStack { }
|
VStack { }
|
||||||
case .other:
|
case .other:
|
||||||
|
@ -107,6 +110,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
Label("timeline.filter.tags", systemImage: iconName)
|
Label("timeline.filter.tags", systemImage: iconName)
|
||||||
case .lists:
|
case .lists:
|
||||||
Label("timeline.filter.lists", systemImage: iconName)
|
Label("timeline.filter.lists", systemImage: iconName)
|
||||||
|
case .links:
|
||||||
|
Label("explore.section.trending.links", systemImage: iconName)
|
||||||
case .other:
|
case .other:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
|
||||||
|
@ -145,6 +150,8 @@ enum Tab: Int, Identifiable, Hashable, CaseIterable, Codable {
|
||||||
"tag"
|
"tag"
|
||||||
case .lists:
|
case .lists:
|
||||||
"list.bullet"
|
"list.bullet"
|
||||||
|
case .links:
|
||||||
|
"newspaper"
|
||||||
case .other:
|
case .other:
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
|
|
|
@ -76555,6 +76555,35 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"trending-tag-people-talking %lld" : {
|
||||||
|
"extractionState" : "manual",
|
||||||
|
"localizations" : {
|
||||||
|
"be" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "%lld people talking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "%lld people talking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"en-GB" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "%lld people talking"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"eu" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "%lld people talking"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"version" : "1.0"
|
"version" : "1.0"
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
public struct TagChartView: View {
|
public struct TagChartView: View {
|
||||||
@State private var sortedHistory: [Tag.History] = []
|
@State private var sortedHistory: [History] = []
|
||||||
|
|
||||||
public init(tag: Tag) {
|
public init(tag: Tag) {
|
||||||
_sortedHistory = .init(initialValue: tag.history.sorted {
|
_sortedHistory = .init(initialValue: tag.history.sorted {
|
||||||
|
|
|
@ -31,6 +31,7 @@ public enum WindowDestinationEditor: Hashable, Codable {
|
||||||
case replyToStatusEditor(status: Status)
|
case replyToStatusEditor(status: Status)
|
||||||
case quoteStatusEditor(status: Status)
|
case quoteStatusEditor(status: Status)
|
||||||
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
||||||
|
case quoteLinkStatusEditor(link: URL)
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum WindowDestinationMedia: Hashable, Codable {
|
public enum WindowDestinationMedia: Hashable, Codable {
|
||||||
|
@ -42,6 +43,7 @@ public enum SheetDestination: Identifiable {
|
||||||
case editStatusEditor(status: Status)
|
case editStatusEditor(status: Status)
|
||||||
case replyToStatusEditor(status: Status)
|
case replyToStatusEditor(status: Status)
|
||||||
case quoteStatusEditor(status: Status)
|
case quoteStatusEditor(status: Status)
|
||||||
|
case quoteLinkStatusEditor(link: URL)
|
||||||
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
case mentionStatusEditor(account: Account, visibility: Models.Visibility)
|
||||||
case listCreate
|
case listCreate
|
||||||
case listEdit(list: Models.List)
|
case listEdit(list: Models.List)
|
||||||
|
@ -62,7 +64,7 @@ public enum SheetDestination: Identifiable {
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
case .editStatusEditor, .newStatusEditor, .replyToStatusEditor, .quoteStatusEditor,
|
||||||
.mentionStatusEditor:
|
.mentionStatusEditor, .quoteLinkStatusEditor:
|
||||||
"statusEditor"
|
"statusEditor"
|
||||||
case .listCreate:
|
case .listCreate:
|
||||||
"listCreate"
|
"listCreate"
|
||||||
|
|
|
@ -1,29 +0,0 @@
|
||||||
import DesignSystem
|
|
||||||
import Models
|
|
||||||
import StatusKit
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
public struct CardsListView: View {
|
|
||||||
@Environment(Theme.self) private var theme
|
|
||||||
|
|
||||||
let cards: [Card]
|
|
||||||
|
|
||||||
public init(cards: [Card]) {
|
|
||||||
self.cards = cards
|
|
||||||
}
|
|
||||||
|
|
||||||
public var body: some View {
|
|
||||||
List {
|
|
||||||
ForEach(cards) { card in
|
|
||||||
StatusRowCardView(card: card)
|
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.scrollContentBackground(.hidden)
|
|
||||||
.background(theme.primaryBackgroundColor)
|
|
||||||
.listStyle(.plain)
|
|
||||||
.navigationTitle("explore.section.trending.links")
|
|
||||||
.navigationBarTitleDisplayMode(.inline)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -98,6 +98,7 @@ public struct ExploreView: View {
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.navigationTitle("explore.navigation-title")
|
.navigationTitle("explore.navigation-title")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
.searchable(text: $viewModel.searchQuery,
|
.searchable(text: $viewModel.searchQuery,
|
||||||
isPresented: $viewModel.isSearchPresented,
|
isPresented: $viewModel.isSearchPresented,
|
||||||
placement: .navigationBarDrawer(displayMode: .always),
|
placement: .navigationBarDrawer(displayMode: .always),
|
||||||
|
@ -125,16 +126,20 @@ public struct ExploreView: View {
|
||||||
private var quickAccessView: some View {
|
private var quickAccessView: some View {
|
||||||
ScrollView(.horizontal) {
|
ScrollView(.horizontal) {
|
||||||
HStack {
|
HStack {
|
||||||
Button("explore.section.trending.tags") {
|
Button("explore.section.trending.links") {
|
||||||
routerPath.navigate(to: RouterDestination.tagsList(tags: viewModel.trendingTags))
|
routerPath.navigate(to: RouterDestination.trendingLinks(cards: viewModel.trendingLinks))
|
||||||
|
}
|
||||||
|
.buttonStyle(.bordered)
|
||||||
|
Button("explore.section.trending.posts") {
|
||||||
|
routerPath.navigate(to: RouterDestination.trendingTimeline)
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
Button("explore.section.suggested-users") {
|
Button("explore.section.suggested-users") {
|
||||||
routerPath.navigate(to: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts))
|
routerPath.navigate(to: RouterDestination.accountsList(accounts: viewModel.suggestedAccounts))
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
Button("explore.section.trending.posts") {
|
Button("explore.section.trending.tags") {
|
||||||
routerPath.navigate(to: RouterDestination.trendingTimeline)
|
routerPath.navigate(to: RouterDestination.tagsList(tags: viewModel.trendingTags))
|
||||||
}
|
}
|
||||||
.buttonStyle(.bordered)
|
.buttonStyle(.bordered)
|
||||||
}
|
}
|
||||||
|
@ -305,6 +310,7 @@ public struct ExploreView: View {
|
||||||
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count))
|
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count))
|
||||||
{ card in
|
{ card in
|
||||||
StatusRowCardView(card: card)
|
StatusRowCardView(card: card)
|
||||||
|
.environment(\.isCompact, true)
|
||||||
#if !os(visionOS)
|
#if !os(visionOS)
|
||||||
.listRowBackground(theme.primaryBackgroundColor)
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
#else
|
#else
|
||||||
|
|
|
@ -88,7 +88,7 @@ import SwiftUI
|
||||||
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
|
async let suggestedAccounts: [Account] = client.get(endpoint: Accounts.suggestions)
|
||||||
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
||||||
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses(offset: nil))
|
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses(offset: nil))
|
||||||
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
|
async let trendingLinks: [Card] = client.get(endpoint: Trends.links(offset: nil))
|
||||||
return try await .init(suggestedAccounts: suggestedAccounts,
|
return try await .init(suggestedAccounts: suggestedAccounts,
|
||||||
trendingTags: trendingTags,
|
trendingTags: trendingTags,
|
||||||
trendingStatuses: trendingStatuses,
|
trendingStatuses: trendingStatuses,
|
||||||
|
|
62
Packages/Explore/Sources/Explore/TrendingLinksListView.swift
Normal file
62
Packages/Explore/Sources/Explore/TrendingLinksListView.swift
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
import StatusKit
|
||||||
|
import SwiftUI
|
||||||
|
import Network
|
||||||
|
|
||||||
|
public struct TrendingLinksListView: View {
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(Client.self) private var client
|
||||||
|
|
||||||
|
@State private var links: [Card]
|
||||||
|
@State private var isLoadingNextPage = false
|
||||||
|
|
||||||
|
public init(cards: [Card]) {
|
||||||
|
_links = .init(initialValue: cards)
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
List {
|
||||||
|
ForEach(links) { card in
|
||||||
|
StatusRowCardView(card: card)
|
||||||
|
.environment(\.isCompact, true)
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
ProgressView()
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
defer {
|
||||||
|
isLoadingNextPage = false
|
||||||
|
}
|
||||||
|
guard !isLoadingNextPage else { return }
|
||||||
|
isLoadingNextPage = true
|
||||||
|
do {
|
||||||
|
let nextPage: [Card] = try await client.get(endpoint: Trends.links(offset: links.count))
|
||||||
|
links.append(contentsOf: nextPage)
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if os(visionOS)
|
||||||
|
.listStyle(.insetGrouped)
|
||||||
|
#else
|
||||||
|
.listStyle(.plain)
|
||||||
|
#endif
|
||||||
|
.refreshable {
|
||||||
|
do {
|
||||||
|
links = try await client.get(endpoint: Trends.links(offset: nil))
|
||||||
|
} catch {}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
.navigationTitle("explore.section.trending.links")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,6 +12,7 @@ public struct Card: Codable, Identifiable, Equatable, Hashable {
|
||||||
public let image: URL?
|
public let image: URL?
|
||||||
public let width: CGFloat
|
public let width: CGFloat
|
||||||
public let height: CGFloat
|
public let height: CGFloat
|
||||||
|
public let history: [History]?
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Card: Sendable {}
|
extension Card: Sendable {}
|
||||||
|
|
11
Packages/Models/Sources/Models/History.swift
Normal file
11
Packages/Models/Sources/Models/History.swift
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct History: Codable, Identifiable, Sendable, Equatable, Hashable {
|
||||||
|
public var id: String {
|
||||||
|
day
|
||||||
|
}
|
||||||
|
|
||||||
|
public let day: String
|
||||||
|
public let accounts: String
|
||||||
|
public let uses: String
|
||||||
|
}
|
|
@ -1,16 +1,6 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Tag: Codable, Identifiable, Equatable, Hashable {
|
public struct Tag: Codable, Identifiable, Equatable, Hashable {
|
||||||
public struct History: Codable, Identifiable {
|
|
||||||
public var id: String {
|
|
||||||
day
|
|
||||||
}
|
|
||||||
|
|
||||||
public let day: String
|
|
||||||
public let accounts: String
|
|
||||||
public let uses: String
|
|
||||||
}
|
|
||||||
|
|
||||||
public func hash(into hasher: inout Hasher) {
|
public func hash(into hasher: inout Hasher) {
|
||||||
hasher.combine(name)
|
hasher.combine(name)
|
||||||
}
|
}
|
||||||
|
@ -65,5 +55,4 @@ public struct FeaturedTag: Codable, Identifiable {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension Tag: Sendable {}
|
extension Tag: Sendable {}
|
||||||
extension Tag.History: Sendable {}
|
|
||||||
extension FeaturedTag: Sendable {}
|
extension FeaturedTag: Sendable {}
|
||||||
|
|
|
@ -3,7 +3,7 @@ import Foundation
|
||||||
public enum Trends: Endpoint {
|
public enum Trends: Endpoint {
|
||||||
case tags
|
case tags
|
||||||
case statuses(offset: Int?)
|
case statuses(offset: Int?)
|
||||||
case links
|
case links(offset: Int?)
|
||||||
|
|
||||||
public func path() -> String {
|
public func path() -> String {
|
||||||
switch self {
|
switch self {
|
||||||
|
@ -18,7 +18,7 @@ public enum Trends: Endpoint {
|
||||||
|
|
||||||
public func queryItems() -> [URLQueryItem]? {
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
switch self {
|
switch self {
|
||||||
case let .statuses(offset):
|
case let .statuses(offset), let .links(offset):
|
||||||
if let offset {
|
if let offset {
|
||||||
return [.init(name: "offset", value: String(offset))]
|
return [.init(name: "offset", value: String(offset))]
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ public extension StatusEditor.ViewModel {
|
||||||
case new(visibility: Models.Visibility)
|
case new(visibility: Models.Visibility)
|
||||||
case edit(status: Status)
|
case edit(status: Status)
|
||||||
case quote(status: Status)
|
case quote(status: Status)
|
||||||
|
case quoteLink(link: URL)
|
||||||
case mention(account: Account, visibility: Models.Visibility)
|
case mention(account: Account, visibility: Models.Visibility)
|
||||||
case shareExtension(items: [NSItemProvider])
|
case shareExtension(items: [NSItemProvider])
|
||||||
|
|
||||||
|
@ -40,7 +41,7 @@ public extension StatusEditor.ViewModel {
|
||||||
|
|
||||||
var title: LocalizedStringKey {
|
var title: LocalizedStringKey {
|
||||||
switch self {
|
switch self {
|
||||||
case .new, .mention, .shareExtension:
|
case .new, .mention, .shareExtension, .quoteLink:
|
||||||
"status.editor.mode.new"
|
"status.editor.mode.new"
|
||||||
case .edit:
|
case .edit:
|
||||||
"status.editor.mode.edit"
|
"status.editor.mode.edit"
|
||||||
|
|
|
@ -234,7 +234,7 @@ extension StatusEditor {
|
||||||
language: selectedLanguage,
|
language: selectedLanguage,
|
||||||
mediaAttributes: mediaAttributes)
|
mediaAttributes: mediaAttributes)
|
||||||
switch mode {
|
switch mode {
|
||||||
case .new, .replyTo, .quote, .mention, .shareExtension:
|
case .new, .replyTo, .quote, .mention, .shareExtension, .quoteLink:
|
||||||
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))
|
postStatus = try await client.post(endpoint: Statuses.postStatus(json: data))
|
||||||
if let postStatus {
|
if let postStatus {
|
||||||
StreamWatcher.shared.emmitPostEvent(for: postStatus)
|
StreamWatcher.shared.emmitPostEvent(for: postStatus)
|
||||||
|
@ -362,6 +362,9 @@ extension StatusEditor {
|
||||||
statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)")
|
statusText = .init(string: "\n\nFrom: @\(status.reblog?.account.acct ?? status.account.acct)\n\(url)")
|
||||||
selectedRange = .init(location: 0, length: 0)
|
selectedRange = .init(location: 0, length: 0)
|
||||||
}
|
}
|
||||||
|
case let .quoteLink(link):
|
||||||
|
statusText = .init(string: "\n\n\(link)")
|
||||||
|
selectedRange = .init(location: 0, length: 0)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,13 +3,17 @@ import Models
|
||||||
import Nuke
|
import Nuke
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
public struct StatusRowCardView: View {
|
public struct StatusRowCardView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
|
@Environment(\.openWindow) private var openWindow
|
||||||
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
@Environment(\.isInCaptureMode) private var isInCaptureMode: Bool
|
||||||
|
@Environment(\.isCompact) private var isCompact: Bool
|
||||||
|
|
||||||
@Environment(Theme.self) private var theme
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
|
||||||
let card: Card
|
let card: Card
|
||||||
|
|
||||||
|
@ -32,7 +36,7 @@ public struct StatusRowCardView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var imageHeight: CGFloat {
|
private var imageHeight: CGFloat {
|
||||||
if theme.statusDisplayStyle == .medium {
|
if theme.statusDisplayStyle == .medium || isCompact {
|
||||||
return 100
|
return 100
|
||||||
}
|
}
|
||||||
return 200
|
return 200
|
||||||
|
@ -47,7 +51,9 @@ public struct StatusRowCardView: View {
|
||||||
if let title = card.title, let url = URL(string: card.url) {
|
if let title = card.title, let url = URL(string: card.url) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
let sitesWithIcons = ["apps.apple.com", "music.apple.com", "open.spotify.com"]
|
let sitesWithIcons = ["apps.apple.com", "music.apple.com", "open.spotify.com"]
|
||||||
if (UIDevice.current.userInterfaceIdiom == .pad ||
|
if isCompact {
|
||||||
|
compactLinkPreview(title, url)
|
||||||
|
} else if (UIDevice.current.userInterfaceIdiom == .pad ||
|
||||||
UIDevice.current.userInterfaceIdiom == .mac ||
|
UIDevice.current.userInterfaceIdiom == .mac ||
|
||||||
UIDevice.current.userInterfaceIdiom == .vision),
|
UIDevice.current.userInterfaceIdiom == .vision),
|
||||||
let host = url.host(), sitesWithIcons.contains(host) {
|
let host = url.host(), sitesWithIcons.contains(host) {
|
||||||
|
@ -59,16 +65,20 @@ public struct StatusRowCardView: View {
|
||||||
.frame(maxWidth: maxWidth)
|
.frame(maxWidth: maxWidth)
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
#if os(visionOS)
|
#if os(visionOS)
|
||||||
.background(.background)
|
.if(!isCompact, transform: { view in
|
||||||
|
view.background(.background)
|
||||||
|
})
|
||||||
.hoverEffect()
|
.hoverEffect()
|
||||||
#else
|
#else
|
||||||
.background(theme.secondaryBackgroundColor)
|
.background(isCompact ? .clear : theme.secondaryBackgroundColor)
|
||||||
#endif
|
#endif
|
||||||
.cornerRadius(10)
|
.cornerRadius(isCompact ? 0 : 10)
|
||||||
.overlay(
|
.overlay {
|
||||||
|
if !isCompact {
|
||||||
RoundedRectangle(cornerRadius: 10)
|
RoundedRectangle(cornerRadius: 10)
|
||||||
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
.stroke(.gray.opacity(0.35), lineWidth: 1)
|
||||||
)
|
}
|
||||||
|
}
|
||||||
.contextMenu {
|
.contextMenu {
|
||||||
ShareLink(item: url) {
|
ShareLink(item: url) {
|
||||||
Label("status.card.share", systemImage: "square.and.arrow.up")
|
Label("status.card.share", systemImage: "square.and.arrow.up")
|
||||||
|
@ -116,6 +126,62 @@ public struct StatusRowCardView: View {
|
||||||
.padding(10)
|
.padding(10)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func compactLinkPreview(_ title: String, _ url: URL) -> some View {
|
||||||
|
HStack(alignment: .top) {
|
||||||
|
if let imageURL = card.image, !isInCaptureMode {
|
||||||
|
LazyResizableImage(url: imageURL) { state, _ in
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.aspectRatio(contentMode: .fill)
|
||||||
|
.frame(width: imageHeight, height: imageHeight)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: 8))
|
||||||
|
.clipped()
|
||||||
|
} else if state.isLoading {
|
||||||
|
Rectangle()
|
||||||
|
.fill(Color.gray)
|
||||||
|
.frame(width: imageHeight, height: imageHeight)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// This image is decorative
|
||||||
|
.accessibilityHidden(true)
|
||||||
|
.frame(width: imageHeight, height: imageHeight)
|
||||||
|
}
|
||||||
|
VStack(alignment: .leading, spacing: 6) {
|
||||||
|
Text(title)
|
||||||
|
.font(.scaledHeadline)
|
||||||
|
.lineLimit(3)
|
||||||
|
Text(url.host() ?? url.absoluteString)
|
||||||
|
.font(.scaledFootnote)
|
||||||
|
.foregroundColor(theme.tintColor)
|
||||||
|
.lineLimit(1)
|
||||||
|
if let history = card.history {
|
||||||
|
let uses = history.compactMap{ Int($0.accounts )}.reduce(0, +)
|
||||||
|
HStack(spacing: 4) {
|
||||||
|
Image(systemName: "bubble.left.and.text.bubble.right")
|
||||||
|
Text("trending-tag-people-talking \(uses)")
|
||||||
|
Spacer()
|
||||||
|
Button {
|
||||||
|
#if targetEnvironment(macCatalyst)
|
||||||
|
openWindow(value: WindowDestinationEditor.quoteLinkStatusEditor(link: url))
|
||||||
|
#else
|
||||||
|
routerPath.presentedSheet = .quoteLinkStatusEditor(link: url)
|
||||||
|
#endif
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "quote.opening")
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
.font(.scaledCaption)
|
||||||
|
.foregroundStyle(.secondary)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.top, 12)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func iconLinkPreview(_ title: String, _ url: URL) -> some View {
|
private func iconLinkPreview(_ title: String, _ url: URL) -> some View {
|
||||||
// ..where the image is known to be a square icon
|
// ..where the image is known to be a square icon
|
||||||
HStack {
|
HStack {
|
||||||
|
|
Loading…
Reference in a new issue