mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 17:51:01 +00:00
Add media grid on user profile
This commit is contained in:
parent
123f05538a
commit
9fa19aa132
13 changed files with 180 additions and 14 deletions
|
@ -68,8 +68,8 @@
|
||||||
"kind" : "remoteSourceControl",
|
"kind" : "remoteSourceControl",
|
||||||
"location" : "https://github.com/kean/Nuke",
|
"location" : "https://github.com/kean/Nuke",
|
||||||
"state" : {
|
"state" : {
|
||||||
"revision" : "311016d972aa751ae8ab0cd5897422ebe7db0501",
|
"revision" : "0ead44350d2737db384908569c012fe67c421e4d",
|
||||||
"version" : "12.7.3"
|
"version" : "12.8.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
|
@ -24,6 +24,8 @@ extension View {
|
||||||
AccountDetailView(account: account, scrollToTopSignal: .constant(0))
|
AccountDetailView(account: account, scrollToTopSignal: .constant(0))
|
||||||
case let .accountSettingsWithAccount(account, appAccount):
|
case let .accountSettingsWithAccount(account, appAccount):
|
||||||
AccountSettingsView(account: account, appAccount: appAccount)
|
AccountSettingsView(account: account, appAccount: appAccount)
|
||||||
|
case let .accountMediaGridView(account, initialMedia):
|
||||||
|
AccountDetailMediaGridView(account: account, initialMediaStatuses: initialMedia)
|
||||||
case let .statusDetail(id):
|
case let .statusDetail(id):
|
||||||
StatusDetailView(statusId: id)
|
StatusDetailView(statusId: id)
|
||||||
case let .statusDetailWithStatus(status):
|
case let .statusDetailWithStatus(status):
|
||||||
|
|
|
@ -79,6 +79,22 @@ public struct AccountDetailView: View {
|
||||||
if viewModel.selectedTab == .statuses {
|
if viewModel.selectedTab == .statuses {
|
||||||
pinnedPostsView
|
pinnedPostsView
|
||||||
}
|
}
|
||||||
|
if viewModel.selectedTab == .media {
|
||||||
|
HStack {
|
||||||
|
Label("Media Grid", systemImage: "square.grid.2x2")
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "chevron.right")
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
if let account = viewModel.account {
|
||||||
|
routerPath.navigate(to: .accountMediaGridView(account: account,
|
||||||
|
initialMediaStatuses: viewModel.statusesMedias))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if !os(visionOS)
|
||||||
|
.listRowBackground(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
StatusesListView(fetcher: viewModel,
|
StatusesListView(fetcher: viewModel,
|
||||||
client: client,
|
client: client,
|
||||||
routerPath: routerPath)
|
routerPath: routerPath)
|
||||||
|
|
|
@ -84,6 +84,9 @@ import SwiftUI
|
||||||
private var tabTask: Task<Void, Never>?
|
private var tabTask: Task<Void, Never>?
|
||||||
|
|
||||||
private(set) var statuses: [Status] = []
|
private(set) var statuses: [Status] = []
|
||||||
|
var statusesMedias: [MediaStatus] {
|
||||||
|
statuses.filter{ !$0.mediaAttachments.isEmpty }.flatMap{ $0.asMediaStatus}
|
||||||
|
}
|
||||||
|
|
||||||
var boosts: [Status] = []
|
var boosts: [Status] = []
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,116 @@
|
||||||
|
import SwiftUI
|
||||||
|
import DesignSystem
|
||||||
|
import NukeUI
|
||||||
|
import Env
|
||||||
|
import MediaUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
public struct AccountDetailMediaGridView: View {
|
||||||
|
@Environment(Theme.self) private var theme
|
||||||
|
@Environment(RouterPath.self) private var routerPath
|
||||||
|
@Environment(Client.self) private var client
|
||||||
|
@Environment(QuickLook.self) private var quickLook
|
||||||
|
|
||||||
|
let account: Account
|
||||||
|
@State var mediaStatuses: [MediaStatus]
|
||||||
|
|
||||||
|
public init(account: Account, initialMediaStatuses: [MediaStatus]) {
|
||||||
|
self.account = account
|
||||||
|
self.mediaStatuses = initialMediaStatuses
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
ScrollView(.vertical) {
|
||||||
|
LazyVGrid(columns: [.init(.flexible(minimum: 100), spacing: 4),
|
||||||
|
.init(.flexible(minimum: 100), spacing: 4),
|
||||||
|
.init(.flexible(minimum: 100), spacing: 4)],
|
||||||
|
spacing: 4) {
|
||||||
|
ForEach(mediaStatuses) { status in
|
||||||
|
GeometryReader { proxy in
|
||||||
|
if let url = status.attachment.url {
|
||||||
|
Group {
|
||||||
|
switch status.attachment.supportedType {
|
||||||
|
case .image:
|
||||||
|
LazyImage(url: url, transaction: Transaction(animation: .easeIn)) { state in
|
||||||
|
if let image = state.image {
|
||||||
|
image
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: proxy.size.width, height: proxy.size.width)
|
||||||
|
} else {
|
||||||
|
ProgressView()
|
||||||
|
.frame(width: proxy.size.width, height: proxy.size.width)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.processors([.resize(size: proxy.size)])
|
||||||
|
.transition(.opacity)
|
||||||
|
case .gifv, .video:
|
||||||
|
MediaUIAttachmentVideoView(viewModel: .init(url: url))
|
||||||
|
case .none:
|
||||||
|
EmptyView()
|
||||||
|
case .some(.audio):
|
||||||
|
EmptyView()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
routerPath.navigate(to: .statusDetailWithStatus(status: status.status))
|
||||||
|
}
|
||||||
|
.contextMenu {
|
||||||
|
Button {
|
||||||
|
quickLook.prepareFor(selectedMediaAttachment: status.attachment,
|
||||||
|
mediaAttachments: status.status.mediaAttachments)
|
||||||
|
} label: {
|
||||||
|
Label("Open Media", systemImage: "photo")
|
||||||
|
}
|
||||||
|
MediaUIShareLink(url: url, type: status.attachment.supportedType == .image ? .image : .av)
|
||||||
|
Button {
|
||||||
|
Task {
|
||||||
|
let transferable = MediaUIImageTransferable(url: url)
|
||||||
|
UIPasteboard.general.image = UIImage(data: await transferable.fetchData())
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
|
Label("status.media.contextmenu.copy", systemImage: "doc.on.doc")
|
||||||
|
}
|
||||||
|
Button {
|
||||||
|
UIPasteboard.general.url = url
|
||||||
|
} label: {
|
||||||
|
Label("status.action.copy-link", systemImage: "link")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.clipped()
|
||||||
|
.aspectRatio(1, contentMode: .fit)
|
||||||
|
}
|
||||||
|
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
NextPageView {
|
||||||
|
try await fetchNextPage()
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationTitle(account.displayName ?? "")
|
||||||
|
#if !os(visionOS)
|
||||||
|
.scrollContentBackground(.hidden)
|
||||||
|
.background(theme.primaryBackgroundColor)
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
private func fetchNextPage() async throws {
|
||||||
|
let client = client
|
||||||
|
let newStatuses: [Status] =
|
||||||
|
try await client.get(endpoint: Accounts.statuses(id: account.id,
|
||||||
|
sinceId: mediaStatuses.last?.id,
|
||||||
|
tag: nil,
|
||||||
|
onlyMedia: true,
|
||||||
|
excludeReplies: true,
|
||||||
|
excludeReblogs: true,
|
||||||
|
pinned: nil))
|
||||||
|
mediaStatuses.append(contentsOf: newStatuses.flatMap{ $0.asMediaStatus })
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ let package = Package(
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(name: "Models", path: "../Models"),
|
.package(name: "Models", path: "../Models"),
|
||||||
.package(name: "Env", path: "../Env"),
|
.package(name: "Env", path: "../Env"),
|
||||||
.package(url: "https://github.com/kean/Nuke", exact: "12.7.3"),
|
.package(url: "https://github.com/kean/Nuke", exact: "12.8.0"),
|
||||||
.package(url: "https://github.com/divadretlaw/EmojiText", exact: "4.0.0"),
|
.package(url: "https://github.com/divadretlaw/EmojiText", exact: "4.0.0"),
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
|
|
|
@ -9,6 +9,7 @@ public enum RouterDestination: Hashable {
|
||||||
case accountDetail(id: String)
|
case accountDetail(id: String)
|
||||||
case accountDetailWithAccount(account: Account)
|
case accountDetailWithAccount(account: Account)
|
||||||
case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
|
case accountSettingsWithAccount(account: Account, appAccount: AppAccount)
|
||||||
|
case accountMediaGridView(account: Account, initialMediaStatuses: [MediaStatus])
|
||||||
case statusDetail(id: String)
|
case statusDetail(id: String)
|
||||||
case statusDetailWithStatus(status: Status)
|
case statusDetailWithStatus(status: Status)
|
||||||
case remoteStatusDetail(url: URL)
|
case remoteStatusDetail(url: URL)
|
||||||
|
|
|
@ -1,11 +1,11 @@
|
||||||
import Models
|
import Models
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
enum DisplayType {
|
public enum DisplayType {
|
||||||
case image
|
case image
|
||||||
case av
|
case av
|
||||||
|
|
||||||
init(from attachmentType: MediaAttachment.SupportedType) {
|
public init(from attachmentType: MediaAttachment.SupportedType) {
|
||||||
switch attachmentType {
|
switch attachmentType {
|
||||||
case .image:
|
case .image:
|
||||||
self = .image
|
self = .image
|
||||||
|
|
|
@ -2,12 +2,12 @@ import Models
|
||||||
import NukeUI
|
import NukeUI
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MediaUIAttachmentImageView: View {
|
public struct MediaUIAttachmentImageView: View {
|
||||||
let url: URL
|
public let url: URL
|
||||||
|
|
||||||
@GestureState private var zoom = 1.0
|
@GestureState private var zoom = 1.0
|
||||||
|
|
||||||
var body: some View {
|
public var body: some View {
|
||||||
MediaUIZoomableContainer {
|
MediaUIZoomableContainer {
|
||||||
LazyImage(url: url) { state in
|
LazyImage(url: url) { state in
|
||||||
if let image = state.image {
|
if let image = state.image {
|
||||||
|
|
|
@ -1,10 +1,15 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct MediaUIShareLink: View, @unchecked Sendable {
|
public struct MediaUIShareLink: View, @unchecked Sendable {
|
||||||
let url: URL
|
let url: URL
|
||||||
let type: DisplayType
|
let type: DisplayType
|
||||||
|
|
||||||
var body: some View {
|
public init(url: URL, type: DisplayType) {
|
||||||
|
self.url = url
|
||||||
|
self.type = type
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
if type == .image {
|
if type == .image {
|
||||||
let transferable = MediaUIImageTransferable(url: url)
|
let transferable = MediaUIImageTransferable(url: url)
|
||||||
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
|
ShareLink(item: transferable, preview: .init("status.media.contextmenu.share",
|
||||||
|
|
|
@ -2,10 +2,14 @@ import CoreTransferable
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
struct MediaUIImageTransferable: Codable, Transferable {
|
public struct MediaUIImageTransferable: Codable, Transferable {
|
||||||
let url: URL
|
public let url: URL
|
||||||
|
|
||||||
func fetchData() async -> Data {
|
public init(url: URL) {
|
||||||
|
self.url = url
|
||||||
|
}
|
||||||
|
|
||||||
|
public func fetchData() async -> Data {
|
||||||
do {
|
do {
|
||||||
return try await URLSession.shared.data(from: url).0
|
return try await URLSession.shared.data(from: url).0
|
||||||
} catch {
|
} catch {
|
||||||
|
@ -13,7 +17,7 @@ struct MediaUIImageTransferable: Codable, Transferable {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static var transferRepresentation: some TransferRepresentation {
|
public static var transferRepresentation: some TransferRepresentation {
|
||||||
DataRepresentation(exportedContentType: .jpeg) { transferable in
|
DataRepresentation(exportedContentType: .jpeg) { transferable in
|
||||||
await transferable.fetchData()
|
await transferable.fetchData()
|
||||||
}
|
}
|
||||||
|
|
15
Packages/Models/Sources/Models/MediaStatus.swift
Normal file
15
Packages/Models/Sources/Models/MediaStatus.swift
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public struct MediaStatus: Sendable, Identifiable, Hashable {
|
||||||
|
public var id: String {
|
||||||
|
attachment.id
|
||||||
|
}
|
||||||
|
|
||||||
|
public let status: Status
|
||||||
|
public let attachment: MediaAttachment
|
||||||
|
|
||||||
|
public init(status: Status, attachment: MediaAttachment) {
|
||||||
|
self.status = status
|
||||||
|
self.attachment = attachment
|
||||||
|
}
|
||||||
|
}
|
|
@ -78,6 +78,10 @@ public final class Status: AnyStatus, Codable, Identifiable, Equatable, Hashable
|
||||||
filtered?.first?.filter.filterAction == .hide
|
filtered?.first?.filter.filterAction == .hide
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public var asMediaStatus: [MediaStatus] {
|
||||||
|
mediaAttachments.map{ .init(status: self, attachment: $0)}
|
||||||
|
}
|
||||||
|
|
||||||
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
public init(id: String, content: HTMLString, account: Account, createdAt: ServerDate, editedAt: ServerDate?, reblog: ReblogStatus?, mediaAttachments: [MediaAttachment], mentions: [Mention], repliesCount: Int, reblogsCount: Int, favouritesCount: Int, card: Card?, favourited: Bool?, reblogged: Bool?, pinned: Bool?, bookmarked: Bool?, emojis: [Emoji], url: String?, application: Application?, inReplyToId: String?, inReplyToAccountId: String?, visibility: Visibility, poll: Poll?, spoilerText: HTMLString, filtered: [Filtered]?, sensitive: Bool, language: String?) {
|
||||||
self.id = id
|
self.id = id
|
||||||
self.content = content
|
self.content = content
|
||||||
|
|
Loading…
Reference in a new issue