mirror of
https://github.com/Dimillian/IceCubesApp.git
synced 2024-11-25 09:41:02 +00:00
Explore + Account polish + Status editor WIP
This commit is contained in:
parent
0679559ced
commit
189037b53d
21 changed files with 358 additions and 59 deletions
|
@ -17,6 +17,8 @@
|
||||||
9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; };
|
9F398AA92935FFDB00A889F2 /* Account in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AA82935FFDB00A889F2 /* Account */; };
|
||||||
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; };
|
9F398AAB2935FFDB00A889F2 /* Models in Frameworks */ = {isa = PBXBuildFile; productRef = 9F398AAA2935FFDB00A889F2 /* Models */; };
|
||||||
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTab.swift */; };
|
9F398AB329360A4C00A889F2 /* TimelineTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F398AB229360A4C00A889F2 /* TimelineTab.swift */; };
|
||||||
|
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9F55C68C2955968700F94077 /* ExploreTab.swift */; };
|
||||||
|
9F55C6902955993C00F94077 /* Explore in Frameworks */ = {isa = PBXBuildFile; productRef = 9F55C68F2955993C00F94077 /* Explore */; };
|
||||||
9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; };
|
9F5E581929545BE700A53960 /* Env in Frameworks */ = {isa = PBXBuildFile; productRef = 9F5E581829545BE700A53960 /* Env */; };
|
||||||
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4ACA293783B000772766 /* SettingsTab.swift */; };
|
9FAE4ACB293783B000772766 /* SettingsTab.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9FAE4ACA293783B000772766 /* SettingsTab.swift */; };
|
||||||
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; };
|
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */ = {isa = PBXBuildFile; productRef = 9FAE4ACD29379A5A00772766 /* KeychainSwift */; };
|
||||||
|
@ -41,6 +43,8 @@
|
||||||
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
|
9F398AA52935FE8A00A889F2 /* AppRouteur.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppRouteur.swift; sourceTree = "<group>"; };
|
||||||
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
|
9F398AAC2936005300A889F2 /* Account */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Account; path = Packages/Account; sourceTree = "<group>"; };
|
||||||
9F398AB229360A4C00A889F2 /* TimelineTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTab.swift; sourceTree = "<group>"; };
|
9F398AB229360A4C00A889F2 /* TimelineTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineTab.swift; sourceTree = "<group>"; };
|
||||||
|
9F55C68C2955968700F94077 /* ExploreTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ExploreTab.swift; sourceTree = "<group>"; };
|
||||||
|
9F55C68E295598F900F94077 /* Explore */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Explore; path = Packages/Explore; sourceTree = "<group>"; };
|
||||||
9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; };
|
9F5E581729545B5500A53960 /* Env */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = Env; path = Packages/Env; sourceTree = "<group>"; };
|
||||||
9FAE4AC8293774FF00772766 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
9FAE4AC8293774FF00772766 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
|
||||||
9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
|
9FAE4ACA293783B000772766 /* SettingsTab.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsTab.swift; sourceTree = "<group>"; };
|
||||||
|
@ -58,6 +62,7 @@
|
||||||
isa = PBXFrameworksBuildPhase;
|
isa = PBXFrameworksBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
9F55C6902955993C00F94077 /* Explore in Frameworks */,
|
||||||
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
|
9FAE4ACE29379A5A00772766 /* KeychainSwift in Frameworks */,
|
||||||
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
9F398AA92935FFDB00A889F2 /* Account in Frameworks */,
|
||||||
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
9FBFE64E292A72BD00C250E9 /* Network in Frameworks */,
|
||||||
|
@ -98,6 +103,7 @@
|
||||||
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
9F398AB229360A4C00A889F2 /* TimelineTab.swift */,
|
||||||
9F35DB4629506F6600B3281A /* NotificationTab.swift */,
|
9F35DB4629506F6600B3281A /* NotificationTab.swift */,
|
||||||
9F35DB4B2952005C00B3281A /* AccountTab.swift */,
|
9F35DB4B2952005C00B3281A /* AccountTab.swift */,
|
||||||
|
9F55C68C2955968700F94077 /* ExploreTab.swift */,
|
||||||
);
|
);
|
||||||
path = Tabs;
|
path = Tabs;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
|
@ -119,6 +125,7 @@
|
||||||
9FBFE64C292A72BD00C250E9 /* Frameworks */,
|
9FBFE64C292A72BD00C250E9 /* Frameworks */,
|
||||||
9F398AAC2936005300A889F2 /* Account */,
|
9F398AAC2936005300A889F2 /* Account */,
|
||||||
9F35DB45294FA04C00B3281A /* DesignSystem */,
|
9F35DB45294FA04C00B3281A /* DesignSystem */,
|
||||||
|
9F55C68E295598F900F94077 /* Explore */,
|
||||||
9F5E581729545B5500A53960 /* Env */,
|
9F5E581729545B5500A53960 /* Env */,
|
||||||
9F398AA32935F90100A889F2 /* Models */,
|
9F398AA32935F90100A889F2 /* Models */,
|
||||||
9F29553D292B67B600E0E81B /* Network */,
|
9F29553D292B67B600E0E81B /* Network */,
|
||||||
|
@ -189,6 +196,7 @@
|
||||||
9F35DB43294F9A7D00B3281A /* Status */,
|
9F35DB43294F9A7D00B3281A /* Status */,
|
||||||
9F35DB4929506FA100B3281A /* Notifications */,
|
9F35DB4929506FA100B3281A /* Notifications */,
|
||||||
9F5E581829545BE700A53960 /* Env */,
|
9F5E581829545BE700A53960 /* Env */,
|
||||||
|
9F55C68F2955993C00F94077 /* Explore */,
|
||||||
);
|
);
|
||||||
productName = IceCubesApp;
|
productName = IceCubesApp;
|
||||||
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
productReference = 9FBFE639292A715500C250E9 /* IceCubesApp.app */;
|
||||||
|
@ -256,6 +264,7 @@
|
||||||
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
9FBFE63D292A715500C250E9 /* IceCubesApp.swift in Sources */,
|
||||||
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
|
9F35DB4729506F6600B3281A /* NotificationTab.swift in Sources */,
|
||||||
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
9FAE4AD129379AD600772766 /* AppAccount.swift in Sources */,
|
||||||
|
9F55C68D2955968700F94077 /* ExploreTab.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
};
|
};
|
||||||
|
@ -516,6 +525,10 @@
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Models;
|
productName = Models;
|
||||||
};
|
};
|
||||||
|
9F55C68F2955993C00F94077 /* Explore */ = {
|
||||||
|
isa = XCSwiftPackageProductDependency;
|
||||||
|
productName = Explore;
|
||||||
|
};
|
||||||
9F5E581829545BE700A53960 /* Env */ = {
|
9F5E581829545BE700A53960 /* Env */ = {
|
||||||
isa = XCSwiftPackageProductDependency;
|
isa = XCSwiftPackageProductDependency;
|
||||||
productName = Env;
|
productName = Env;
|
||||||
|
|
|
@ -24,8 +24,8 @@ extension View {
|
||||||
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
|
func withSheetDestinations(sheetDestinations: Binding<SheetDestinations?>) -> some View {
|
||||||
self.sheet(item: sheetDestinations) { destination in
|
self.sheet(item: sheetDestinations) { destination in
|
||||||
switch destination {
|
switch destination {
|
||||||
default:
|
case .statusEditor:
|
||||||
EmptyView()
|
StatusEditorView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -17,13 +17,19 @@ struct IceCubesApp: App {
|
||||||
TabView {
|
TabView {
|
||||||
TimelineTab()
|
TimelineTab()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Home", systemImage: "globe")
|
Label("Timeline", systemImage: "rectangle.on.rectangle")
|
||||||
}
|
}
|
||||||
if appAccountsManager.currentClient.isAuth {
|
if appAccountsManager.currentClient.isAuth {
|
||||||
NotificationsTab()
|
NotificationsTab()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Notifications", systemImage: "bell")
|
Label("Notifications", systemImage: "bell")
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
ExploreTab()
|
||||||
|
.tabItem {
|
||||||
|
Label("Explore", systemImage: "magnifyingglass")
|
||||||
|
}
|
||||||
|
if appAccountsManager.currentClient.isAuth {
|
||||||
AccountTab()
|
AccountTab()
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("Profile", systemImage: "person.circle")
|
Label("Profile", systemImage: "person.circle")
|
||||||
|
|
18
IceCubesApp/App/Tabs/ExploreTab.swift
Normal file
18
IceCubesApp/App/Tabs/ExploreTab.swift
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
import Models
|
||||||
|
import Shimmer
|
||||||
|
import Explore
|
||||||
|
|
||||||
|
struct ExploreTab: View {
|
||||||
|
@StateObject private var routeurPath = RouterPath()
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
NavigationStack(path: $routeurPath.path) {
|
||||||
|
ExploreView()
|
||||||
|
.withAppRouteur()
|
||||||
|
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||||
|
}
|
||||||
|
.environmentObject(routeurPath)
|
||||||
|
}
|
||||||
|
}
|
|
@ -11,6 +11,16 @@ struct TimelineTab: View {
|
||||||
TimelineView()
|
TimelineView()
|
||||||
.withAppRouteur()
|
.withAppRouteur()
|
||||||
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
.withSheetDestinations(sheetDestinations: $routeurPath.presentedSheet)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
routeurPath.presentedSheet = .statusEditor(replyToStatus: nil)
|
||||||
|
} label: {
|
||||||
|
Image(systemName: "square.and.pencil")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.environmentObject(routeurPath)
|
.environmentObject(routeurPath)
|
||||||
}
|
}
|
||||||
|
|
|
@ -66,24 +66,11 @@ struct AccountDetailHeaderView: View {
|
||||||
|
|
||||||
private var accountAvatarView: some View {
|
private var accountAvatarView: some View {
|
||||||
HStack {
|
HStack {
|
||||||
AsyncImage(
|
AvatarView(url: account.avatar, size: .account)
|
||||||
url: account.avatar,
|
.overlay(
|
||||||
content: { image in
|
RoundedRectangle(cornerRadius: 4)
|
||||||
image.resizable()
|
.stroke(.white, lineWidth: 1)
|
||||||
.aspectRatio(contentMode: .fit)
|
)
|
||||||
.cornerRadius(4)
|
|
||||||
.frame(maxWidth: 80, maxHeight: 80)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 4)
|
|
||||||
.stroke(.white, lineWidth: 1)
|
|
||||||
)
|
|
||||||
},
|
|
||||||
placeholder: {
|
|
||||||
ProgressView()
|
|
||||||
.frame(maxWidth: 80, maxHeight: 80)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
Task {
|
Task {
|
||||||
await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar)
|
await quickLook.prepareFor(urls: [account.avatar], selectedURL: account.avatar)
|
||||||
|
|
|
@ -77,7 +77,7 @@ public struct AccountDetailView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.edgesIgnoringSafeArea(.top)
|
.edgesIgnoringSafeArea(.top)
|
||||||
.navigationTitle(Text(scrollOffset < -20 ? viewModel.title : ""))
|
.navigationTitle(Text(scrollOffset < -200 ? viewModel.title : ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
|
@ -205,21 +205,9 @@ public struct AccountDetailView: View {
|
||||||
private func makeTagsListView(tags: [Tag]) -> some View {
|
private func makeTagsListView(tags: [Tag]) -> some View {
|
||||||
Group {
|
Group {
|
||||||
ForEach(tags) { tag in
|
ForEach(tags) { tag in
|
||||||
HStack {
|
TagRowView(tag: tag)
|
||||||
VStack(alignment: .leading) {
|
.padding(.horizontal, DS.Constants.layoutPadding)
|
||||||
Text("#\(tag.name)")
|
.padding(.vertical, 8)
|
||||||
.font(.headline)
|
|
||||||
Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants")
|
|
||||||
.font(.footnote)
|
|
||||||
.foregroundColor(.gray)
|
|
||||||
}
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, DS.Constants.layoutPadding)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.onTapGesture {
|
|
||||||
routeurPath.navigate(to: .hashTag(tag: tag.name, account: nil))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,30 +3,41 @@ import Shimmer
|
||||||
|
|
||||||
public struct AvatarView: View {
|
public struct AvatarView: View {
|
||||||
public enum Size {
|
public enum Size {
|
||||||
case profile, badge
|
case account, status, badge
|
||||||
|
|
||||||
var size: CGSize {
|
var size: CGSize {
|
||||||
switch self {
|
switch self {
|
||||||
case .profile:
|
case .account:
|
||||||
|
return .init(width: 80, height: 80)
|
||||||
|
case .status:
|
||||||
return .init(width: 40, height: 40)
|
return .init(width: 40, height: 40)
|
||||||
case .badge:
|
case .badge:
|
||||||
return .init(width: 28, height: 28)
|
return .init(width: 28, height: 28)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cornerRadius: CGFloat {
|
||||||
|
switch self {
|
||||||
|
case .badge:
|
||||||
|
return size.width / 2
|
||||||
|
default:
|
||||||
|
return 4
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Environment(\.redactionReasons) private var reasons
|
@Environment(\.redactionReasons) private var reasons
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let size: Size
|
public let size: Size
|
||||||
|
|
||||||
public init(url: URL, size: Size = .profile) {
|
public init(url: URL, size: Size = .status) {
|
||||||
self.url = url
|
self.url = url
|
||||||
self.size = size
|
self.size = size
|
||||||
}
|
}
|
||||||
|
|
||||||
public var body: some View {
|
public var body: some View {
|
||||||
if reasons == .placeholder {
|
if reasons == .placeholder {
|
||||||
RoundedRectangle(cornerRadius: size == .profile ? 4 : size.size.width / 2)
|
RoundedRectangle(cornerRadius: size.cornerRadius)
|
||||||
.fill(.gray)
|
.fill(.gray)
|
||||||
.frame(maxWidth: size.size.width, maxHeight: size.size.height)
|
.frame(maxWidth: size.size.width, maxHeight: size.size.height)
|
||||||
} else {
|
} else {
|
||||||
|
@ -39,7 +50,7 @@ public struct AvatarView: View {
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
.frame(width: size.size.width, height: size.size.height)
|
||||||
.shimmering()
|
.shimmering()
|
||||||
} else {
|
} else {
|
||||||
RoundedRectangle(cornerRadius: size == .profile ? 4 : size.size.width / 2)
|
RoundedRectangle(cornerRadius: size.cornerRadius)
|
||||||
.fill(.gray)
|
.fill(.gray)
|
||||||
.frame(width: size.size.width, height: size.size.height)
|
.frame(width: size.size.width, height: size.size.height)
|
||||||
.shimmering()
|
.shimmering()
|
||||||
|
@ -47,7 +58,7 @@ public struct AvatarView: View {
|
||||||
case let .success(image):
|
case let .success(image):
|
||||||
image.resizable()
|
image.resizable()
|
||||||
.aspectRatio(contentMode: .fit)
|
.aspectRatio(contentMode: .fit)
|
||||||
.cornerRadius(size == .profile ? 4 : size.size.width / 2)
|
.cornerRadius(size.cornerRadius)
|
||||||
.frame(maxWidth: size.size.width, maxHeight: size.size.height)
|
.frame(maxWidth: size.size.width, maxHeight: size.size.height)
|
||||||
case .failure:
|
case .failure:
|
||||||
EmptyView()
|
EmptyView()
|
||||||
|
|
|
@ -0,0 +1,29 @@
|
||||||
|
import Models
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
|
||||||
|
public struct TagRowView: View {
|
||||||
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
|
let tag: Tag
|
||||||
|
|
||||||
|
public init(tag: Tag) {
|
||||||
|
self.tag = tag
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
HStack {
|
||||||
|
VStack(alignment: .leading) {
|
||||||
|
Text("#\(tag.name)")
|
||||||
|
.font(.headline)
|
||||||
|
Text("\(tag.totalUses) posts from \(tag.totalAccounts) participants")
|
||||||
|
.font(.footnote)
|
||||||
|
.foregroundColor(.gray)
|
||||||
|
}
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
routeurPath.navigate(to: .hashTag(tag: tag.name, account: nil))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,10 +10,12 @@ public enum RouteurDestinations: Hashable {
|
||||||
}
|
}
|
||||||
|
|
||||||
public enum SheetDestinations: Identifiable {
|
public enum SheetDestinations: Identifiable {
|
||||||
|
case statusEditor(replyToStatus: String?)
|
||||||
|
|
||||||
public var id: String {
|
public var id: String {
|
||||||
switch self {
|
switch self {
|
||||||
default:
|
case .statusEditor:
|
||||||
break
|
return "statusEditor"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
9
Packages/Explore/.gitignore
vendored
Normal file
9
Packages/Explore/.gitignore
vendored
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.DS_Store
|
||||||
|
/.build
|
||||||
|
/Packages
|
||||||
|
/*.xcodeproj
|
||||||
|
xcuserdata/
|
||||||
|
DerivedData/
|
||||||
|
.swiftpm/config/registries.json
|
||||||
|
.swiftpm/xcode/package.xcworkspace/contents.xcworkspacedata
|
||||||
|
.netrc
|
35
Packages/Explore/Package.swift
Normal file
35
Packages/Explore/Package.swift
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
// swift-tools-version: 5.7
|
||||||
|
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||||
|
|
||||||
|
import PackageDescription
|
||||||
|
|
||||||
|
let package = Package(
|
||||||
|
name: "Explore",
|
||||||
|
platforms: [
|
||||||
|
.iOS(.v16),
|
||||||
|
],
|
||||||
|
products: [
|
||||||
|
.library(
|
||||||
|
name: "Explore",
|
||||||
|
targets: ["Explore"]),
|
||||||
|
],
|
||||||
|
dependencies: [
|
||||||
|
.package(name: "Network", path: "../Network"),
|
||||||
|
.package(name: "Models", path: "../Models"),
|
||||||
|
.package(name: "Env", path: "../Env"),
|
||||||
|
.package(name: "Status", path: "../Status"),
|
||||||
|
.package(url: "https://github.com/markiv/SwiftUI-Shimmer", exact: "1.1.0")
|
||||||
|
],
|
||||||
|
targets: [
|
||||||
|
.target(
|
||||||
|
name: "Explore",
|
||||||
|
dependencies: [
|
||||||
|
.product(name: "Network", package: "Network"),
|
||||||
|
.product(name: "Models", package: "Models"),
|
||||||
|
.product(name: "Env", package: "Env"),
|
||||||
|
.product(name: "Status", package: "Status"),
|
||||||
|
.product(name: "Shimmer", package: "SwiftUI-Shimmer")
|
||||||
|
])
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
3
Packages/Explore/README.md
Normal file
3
Packages/Explore/README.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Explore
|
||||||
|
|
||||||
|
A description of this package.
|
95
Packages/Explore/Sources/Explore/ExploreView.swift
Normal file
95
Packages/Explore/Sources/Explore/ExploreView.swift
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Env
|
||||||
|
import Network
|
||||||
|
import DesignSystem
|
||||||
|
import Models
|
||||||
|
import Status
|
||||||
|
|
||||||
|
public struct ExploreView: View {
|
||||||
|
@EnvironmentObject private var client: Client
|
||||||
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
|
|
||||||
|
@StateObject private var viewModel = ExploreViewModel()
|
||||||
|
@State private var searchQuery: String = ""
|
||||||
|
|
||||||
|
public init() { }
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
List {
|
||||||
|
Section("Trending Tags") {
|
||||||
|
ForEach(viewModel.trendingTags
|
||||||
|
.prefix(upTo: viewModel.trendingTags.count > 5 ? 5 : viewModel.trendingTags.count)) { tag in
|
||||||
|
TagRowView(tag: tag)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.trendingTags) { tag in
|
||||||
|
TagRowView(tag: tag)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Trending Tags")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Trending Posts") {
|
||||||
|
ForEach(viewModel.trendingStatuses
|
||||||
|
.prefix(upTo: viewModel.trendingStatuses.count > 3 ? 3 : viewModel.trendingStatuses.count)) { status in
|
||||||
|
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.trendingStatuses) { status in
|
||||||
|
StatusRowView(viewModel: .init(status: status, isEmbed: false))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Trending Posts")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Section("Trending Links") {
|
||||||
|
ForEach(viewModel.trendingLinks
|
||||||
|
.prefix(upTo: viewModel.trendingLinks.count > 3 ? 3 : viewModel.trendingLinks.count)) { card in
|
||||||
|
StatusCardView(card: card)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
NavigationLink {
|
||||||
|
List {
|
||||||
|
ForEach(viewModel.trendingLinks) { card in
|
||||||
|
StatusCardView(card: card)
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listStyle(.plain)
|
||||||
|
.navigationTitle("Trending Links")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
} label: {
|
||||||
|
Text("See more")
|
||||||
|
.foregroundColor(.brand)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.task {
|
||||||
|
viewModel.client = client
|
||||||
|
await viewModel.fetchTrending()
|
||||||
|
}
|
||||||
|
.listStyle(.grouped)
|
||||||
|
.navigationTitle("Explore")
|
||||||
|
.searchable(text: $searchQuery)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
25
Packages/Explore/Sources/Explore/ExploreViewModel.swift
Normal file
25
Packages/Explore/Sources/Explore/ExploreViewModel.swift
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import SwiftUI
|
||||||
|
import Models
|
||||||
|
import Network
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
class ExploreViewModel: ObservableObject {
|
||||||
|
var client: Client?
|
||||||
|
|
||||||
|
@Published var trendingTags: [Tag] = []
|
||||||
|
@Published var trendingStatuses: [Status] = []
|
||||||
|
@Published var trendingLinks: [Card] = []
|
||||||
|
|
||||||
|
func fetchTrending() async {
|
||||||
|
guard let client else { return }
|
||||||
|
do {
|
||||||
|
async let trendingTags: [Tag] = client.get(endpoint: Trends.tags)
|
||||||
|
async let trendingStatuses: [Status] = client.get(endpoint: Trends.statuses)
|
||||||
|
async let trendingLinks: [Card] = client.get(endpoint: Trends.links)
|
||||||
|
|
||||||
|
self.trendingTags = try await trendingTags
|
||||||
|
self.trendingStatuses = try await trendingStatuses
|
||||||
|
self.trendingLinks = try await trendingLinks
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,6 +1,10 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public struct Card: Codable {
|
public struct Card: Codable, Identifiable {
|
||||||
|
public var id: String {
|
||||||
|
url.absoluteString
|
||||||
|
}
|
||||||
|
|
||||||
public let url: URL
|
public let url: URL
|
||||||
public let title: String?
|
public let title: String?
|
||||||
public let description: String?
|
public let description: String?
|
||||||
|
|
25
Packages/Network/Sources/Network/Endpoint/Trends.swift
Normal file
25
Packages/Network/Sources/Network/Endpoint/Trends.swift
Normal file
|
@ -0,0 +1,25 @@
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public enum Trends: Endpoint {
|
||||||
|
case tags
|
||||||
|
case statuses
|
||||||
|
case links
|
||||||
|
|
||||||
|
public func path() -> String {
|
||||||
|
switch self {
|
||||||
|
case .tags:
|
||||||
|
return "trends/tags"
|
||||||
|
case .statuses:
|
||||||
|
return "trends/statuses"
|
||||||
|
case .links:
|
||||||
|
return "trends/links"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public func queryItems() -> [URLQueryItem]? {
|
||||||
|
switch self {
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
36
Packages/Status/Sources/Status/Editor/StatusEditorView.swift
Normal file
36
Packages/Status/Sources/Status/Editor/StatusEditorView.swift
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
public struct StatusEditorView: View {
|
||||||
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
|
@State private var statusText: String = ""
|
||||||
|
public init() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
NavigationStack {
|
||||||
|
Form {
|
||||||
|
TextEditor(text: $statusText)
|
||||||
|
}
|
||||||
|
.navigationTitle("Post a toot")
|
||||||
|
.navigationBarTitleDisplayMode(.inline)
|
||||||
|
.toolbar {
|
||||||
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
Button {
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Text("Post")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
|
Button {
|
||||||
|
dismiss()
|
||||||
|
} label: {
|
||||||
|
Text("Cancel")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -4,6 +4,7 @@ import Env
|
||||||
import Network
|
import Network
|
||||||
|
|
||||||
struct StatusActionsView: View {
|
struct StatusActionsView: View {
|
||||||
|
@EnvironmentObject private var routeurPath: RouterPath
|
||||||
@ObservedObject var viewModel: StatusRowViewModel
|
@ObservedObject var viewModel: StatusRowViewModel
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
|
@ -62,6 +63,8 @@ struct StatusActionsView: View {
|
||||||
private func handleAction(action: Actions) {
|
private func handleAction(action: Actions) {
|
||||||
Task {
|
Task {
|
||||||
switch action {
|
switch action {
|
||||||
|
case .respond:
|
||||||
|
routeurPath.navigate(to: .statusDetail(id: viewModel.status.reblog?.id ?? viewModel.status.id))
|
||||||
case .favourite:
|
case .favourite:
|
||||||
if viewModel.isFavourited {
|
if viewModel.isFavourited {
|
||||||
await viewModel.unFavourite()
|
await viewModel.unFavourite()
|
||||||
|
|
|
@ -2,12 +2,16 @@ import SwiftUI
|
||||||
import Models
|
import Models
|
||||||
import Shimmer
|
import Shimmer
|
||||||
|
|
||||||
struct StatusCardView: View {
|
public struct StatusCardView: View {
|
||||||
@Environment(\.openURL) private var openURL
|
@Environment(\.openURL) private var openURL
|
||||||
let status: AnyStatus
|
let card: Card
|
||||||
|
|
||||||
var body: some View {
|
public init(card: Card) {
|
||||||
if let card = status.card, let title = card.title {
|
self.card = card
|
||||||
|
}
|
||||||
|
|
||||||
|
public var body: some View {
|
||||||
|
if let title = card.title {
|
||||||
VStack(alignment: .leading) {
|
VStack(alignment: .leading) {
|
||||||
if let imageURL = card.image {
|
if let imageURL = card.image {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
|
@ -59,9 +63,3 @@ struct StatusCardView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
struct StatusCardView_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
StatusCardView(status: Status.placeholder())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -67,14 +67,16 @@ public struct StatusRowView: View {
|
||||||
StatusMediaPreviewView(attachements: status.mediaAttachments)
|
StatusMediaPreviewView(attachements: status.mediaAttachments)
|
||||||
.padding(.vertical, 4)
|
.padding(.vertical, 4)
|
||||||
}
|
}
|
||||||
StatusCardView(status: status)
|
if let card = status.card {
|
||||||
|
StatusCardView(card: card)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private func makeAccountView(status: AnyStatus) -> some View {
|
private func makeAccountView(status: AnyStatus) -> some View {
|
||||||
AvatarView(url: status.account.avatar)
|
AvatarView(url: status.account.avatar, size: .status)
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
status.account.displayNameWithEmojis
|
status.account.displayNameWithEmojis
|
||||||
.font(.subheadline)
|
.font(.subheadline)
|
||||||
|
|
Loading…
Reference in a new issue