mirror of
https://github.com/lumaa-dev/BubbleApp.git
synced 2025-02-02 11:37:05 +01:00
Attachments, settings, begin of quotes...
This commit is contained in:
parent
7aae807bd8
commit
951b60a846
@ -25,11 +25,17 @@
|
||||
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */; };
|
||||
B9842C162B2F363600D9F3C1 /* TimelineFilter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */; };
|
||||
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9842C172B2F36F500D9F3C1 /* AccountsList.swift */; };
|
||||
B98BC7472B46CE6300595441 /* PostDetailsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC7462B46CE6300595441 /* PostDetailsView.swift */; };
|
||||
B98BC7492B46CEDA00595441 /* AppearenceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC7482B46CEDA00595441 /* AppearenceView.swift */; };
|
||||
B98BC74B2B46CF0400595441 /* ListStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC74A2B46CF0400595441 /* ListStyle.swift */; };
|
||||
B98BC74D2B46CFCE00595441 /* UserPreferences.swift in Sources */ = {isa = PBXBuildFile; fileRef = B98BC74C2B46CFCE00595441 /* UserPreferences.swift */; };
|
||||
B9B63B212B442D1500BBC82D /* DynamicTextEditor.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */; };
|
||||
B9B63B232B447B8000BBC82D /* PostCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B222B447B8000BBC82D /* PostCardView.swift */; };
|
||||
B9B63B252B44997400BBC82D /* QuotePostView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B242B44997400BBC82D /* QuotePostView.swift */; };
|
||||
B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9B63B262B449CDC00BBC82D /* SearchResults.swift */; };
|
||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */; };
|
||||
B9EBE8562B47256900FB594D /* PostAttachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EBE8552B47256900FB594D /* PostAttachment.swift */; };
|
||||
B9EBE8582B474FD600FB594D /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9EBE8572B474FD600FB594D /* AppDelegate.swift */; };
|
||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */; };
|
||||
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B9FB945C2B2DEECE00D81C07 /* ContentView.swift */; };
|
||||
B9FB94612B2DEECF00D81C07 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = B9FB94602B2DEECF00D81C07 /* Assets.xcassets */; };
|
||||
@ -99,12 +105,18 @@
|
||||
B9842C132B2F310C00D9F3C1 /* FetchTimeline.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FetchTimeline.swift; sourceTree = "<group>"; };
|
||||
B9842C152B2F363600D9F3C1 /* TimelineFilter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineFilter.swift; sourceTree = "<group>"; };
|
||||
B9842C172B2F36F500D9F3C1 /* AccountsList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountsList.swift; sourceTree = "<group>"; };
|
||||
B98BC7462B46CE6300595441 /* PostDetailsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostDetailsView.swift; sourceTree = "<group>"; };
|
||||
B98BC7482B46CEDA00595441 /* AppearenceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppearenceView.swift; sourceTree = "<group>"; };
|
||||
B98BC74A2B46CF0400595441 /* ListStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListStyle.swift; sourceTree = "<group>"; };
|
||||
B98BC74C2B46CFCE00595441 /* UserPreferences.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserPreferences.swift; sourceTree = "<group>"; };
|
||||
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicTextEditor.swift; sourceTree = "<group>"; };
|
||||
B9B63B222B447B8000BBC82D /* PostCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostCardView.swift; sourceTree = "<group>"; };
|
||||
B9B63B242B44997400BBC82D /* QuotePostView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuotePostView.swift; sourceTree = "<group>"; };
|
||||
B9B63B262B449CDC00BBC82D /* SearchResults.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchResults.swift; sourceTree = "<group>"; };
|
||||
B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutView.swift; sourceTree = "<group>"; };
|
||||
B9CC45B92B40AA1E001E4FA5 /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
|
||||
B9EBE8552B47256900FB594D /* PostAttachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostAttachment.swift; sourceTree = "<group>"; };
|
||||
B9EBE8572B474FD600FB594D /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
|
||||
B9FB94572B2DEECE00D81C07 /* Threaded.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Threaded.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThreadedApp.swift; sourceTree = "<group>"; };
|
||||
B9FB945C2B2DEECE00D81C07 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
|
||||
@ -190,6 +202,7 @@
|
||||
children = (
|
||||
B9FB94A02B2EF23100D81C07 /* Info.plist */,
|
||||
B9FB945A2B2DEECE00D81C07 /* ThreadedApp.swift */,
|
||||
B9EBE8572B474FD600FB594D /* AppDelegate.swift */,
|
||||
B9FB946E2B2DF3BB00D81C07 /* Components */,
|
||||
B9FB946D2B2DF3B800D81C07 /* Views */,
|
||||
B9FB946C2B2DF3A600D81C07 /* Data */,
|
||||
@ -214,6 +227,7 @@
|
||||
B9FB94BD2B2F038D00D81C07 /* Accounts */,
|
||||
B9FB946F2B2DF3CD00D81C07 /* Navigator.swift */,
|
||||
B9FB94A12B2EF24A00D81C07 /* AppInfo.swift */,
|
||||
B98BC74C2B46CFCE00595441 /* UserPreferences.swift */,
|
||||
B9FB94BB2B2F035500D81C07 /* Tag.swift */,
|
||||
B9FB94802B2E1FEF00D81C07 /* HTMLString.swift */,
|
||||
B9FB94872B2E223E00D81C07 /* Emoji.swift */,
|
||||
@ -240,6 +254,7 @@
|
||||
B9842C112B2F2A5800D9F3C1 /* TimelineView.swift */,
|
||||
B97BCE252B3DE5A10044756D /* AccountView.swift */,
|
||||
B93B677B2B433A6E000892E9 /* PostingView.swift */,
|
||||
B98BC7462B46CE6300595441 /* PostDetailsView.swift */,
|
||||
);
|
||||
path = Views;
|
||||
sourceTree = "<group>";
|
||||
@ -256,6 +271,8 @@
|
||||
B9B63B202B442D1500BBC82D /* DynamicTextEditor.swift */,
|
||||
B9B63B222B447B8000BBC82D /* PostCardView.swift */,
|
||||
B9B63B242B44997400BBC82D /* QuotePostView.swift */,
|
||||
B98BC74A2B46CF0400595441 /* ListStyle.swift */,
|
||||
B9EBE8552B47256900FB594D /* PostAttachment.swift */,
|
||||
);
|
||||
path = Components;
|
||||
sourceTree = "<group>";
|
||||
@ -266,6 +283,7 @@
|
||||
B9FB94912B2E35D000D81C07 /* SettingsView.swift */,
|
||||
B9FB94962B2EDABF00D81C07 /* PrivacyView.swift */,
|
||||
B9CC45B72B40A2D6001E4FA5 /* AboutView.swift */,
|
||||
B98BC7482B46CEDA00595441 /* AppearenceView.swift */,
|
||||
);
|
||||
path = Settings;
|
||||
sourceTree = "<group>";
|
||||
@ -417,6 +435,7 @@
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
B9FB94922B2E35D000D81C07 /* SettingsView.swift in Sources */,
|
||||
B98BC7472B46CE6300595441 /* PostDetailsView.swift in Sources */,
|
||||
B9CC45B82B40A2D6001E4FA5 /* AboutView.swift in Sources */,
|
||||
B9FB94882B2E223E00D81C07 /* Emoji.swift in Sources */,
|
||||
B93B67782B42E8F0000892E9 /* TextEmoji.swift in Sources */,
|
||||
@ -424,6 +443,7 @@
|
||||
B9FB947D2B2E19E300D81C07 /* AccountManager.swift in Sources */,
|
||||
B9FB945D2B2DEECE00D81C07 /* ContentView.swift in Sources */,
|
||||
B9842C0E2B2F21B700D9F3C1 /* CompactPostView.swift in Sources */,
|
||||
B98BC7492B46CEDA00595441 /* AppearenceView.swift in Sources */,
|
||||
B9FB94992B2EEB9400D81C07 /* AddInstanceView.swift in Sources */,
|
||||
B9FB94972B2EDABF00D81C07 /* PrivacyView.swift in Sources */,
|
||||
B9842C142B2F310C00D9F3C1 /* FetchTimeline.swift in Sources */,
|
||||
@ -437,12 +457,15 @@
|
||||
B9FB945B2B2DEECE00D81C07 /* ThreadedApp.swift in Sources */,
|
||||
B9FB94862B2E211200D81C07 /* Account+Elms.swift in Sources */,
|
||||
B9FB94BC2B2F035500D81C07 /* Tag.swift in Sources */,
|
||||
B98BC74B2B46CF0400595441 /* ListStyle.swift in Sources */,
|
||||
B9FB94812B2E1FEF00D81C07 /* HTMLString.swift in Sources */,
|
||||
B9FB947F2B2E1D5F00D81C07 /* Account.swift in Sources */,
|
||||
B9842C122B2F2A5800D9F3C1 /* TimelineView.swift in Sources */,
|
||||
B9FB948C2B2E232300D81C07 /* OnlineImage.swift in Sources */,
|
||||
B9FB94742B2DF6A100D81C07 /* ButtonStyles.swift in Sources */,
|
||||
B9FB94702B2DF3CD00D81C07 /* Navigator.swift in Sources */,
|
||||
B9EBE8562B47256900FB594D /* PostAttachment.swift in Sources */,
|
||||
B9EBE8582B474FD600FB594D /* AppDelegate.swift in Sources */,
|
||||
B93B677C2B433A6E000892E9 /* PostingView.swift in Sources */,
|
||||
B97BCE262B3DE5A10044756D /* AccountView.swift in Sources */,
|
||||
B9B63B272B449CDC00BBC82D /* SearchResults.swift in Sources */,
|
||||
@ -452,6 +475,7 @@
|
||||
B9FB949F2B2EF0F200D81C07 /* MastodonRequest.swift in Sources */,
|
||||
B9842C182B2F36F500D9F3C1 /* AccountsList.swift in Sources */,
|
||||
B9FB948E2B2E28E800D81C07 /* ShareableImage.swift in Sources */,
|
||||
B98BC74D2B46CFCE00595441 /* UserPreferences.swift in Sources */,
|
||||
B9FB94A22B2EF24A00D81C07 /* AppInfo.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
|
@ -1,13 +1,48 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
|
||||
struct AppDelegate: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
@Observable
|
||||
public class AppDelegate: NSObject, UIWindowSceneDelegate, Sendable, UIApplicationDelegate {
|
||||
public var window: UIWindow?
|
||||
public private(set) var windowWidth: CGFloat = UIScreen.main.bounds.size.width
|
||||
public private(set) var windowHeight: CGFloat = UIScreen.main.bounds.size.height
|
||||
|
||||
public func scene(_ scene: UIScene, willConnectTo _: UISceneSession, options _: UIScene.ConnectionOptions) {
|
||||
guard let windowScene = scene as? UIWindowScene else { return }
|
||||
window = windowScene.keyWindow
|
||||
}
|
||||
|
||||
override public init() {
|
||||
super.init()
|
||||
windowWidth = window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
||||
windowHeight = window?.bounds.size.height ?? UIScreen.main.bounds.size.height
|
||||
Self.observedSceneDelegate.insert(self)
|
||||
_ = Self.observer // just for activating the lazy static property
|
||||
}
|
||||
|
||||
deinit {
|
||||
Task { @MainActor in
|
||||
Self.observedSceneDelegate.remove(self)
|
||||
}
|
||||
}
|
||||
|
||||
private static var observedSceneDelegate: Set<AppDelegate> = []
|
||||
private static let observer = Task {
|
||||
while true {
|
||||
try? await Task.sleep(for: .seconds(0.1))
|
||||
for delegate in observedSceneDelegate {
|
||||
let newWidth = delegate.window?.bounds.size.width ?? UIScreen.main.bounds.size.width
|
||||
if delegate.windowWidth != newWidth {
|
||||
delegate.windowWidth = newWidth
|
||||
}
|
||||
let newHeight = delegate.window?.bounds.size.height ?? UIScreen.main.bounds.size.height
|
||||
if delegate.windowHeight != newHeight {
|
||||
delegate.windowHeight = newHeight
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
AppDelegate()
|
||||
}
|
||||
|
@ -6,8 +6,10 @@ struct CompactPostView: View {
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
var status: Status
|
||||
@ObservedObject var navigator: Navigator
|
||||
|
||||
var pinned: Bool = false
|
||||
|
||||
@State private var preferences: UserPreferences = .defaultPreferences
|
||||
@State private var initialLike: Bool = false
|
||||
@State private var isLiked: Bool = false
|
||||
@State private var isReposted: Bool = false
|
||||
@ -36,13 +38,18 @@ struct CompactPostView: View {
|
||||
.padding(.bottom, 3)
|
||||
}
|
||||
.onAppear {
|
||||
do {
|
||||
preferences = try UserPreferences.loadAsCurrent() ?? UserPreferences.defaultPreferences
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
isLiked = status.reblog != nil ? status.reblog!.favourited ?? false : status.favourited ?? false
|
||||
initialLike = isLiked
|
||||
isReposted = status.reblog != nil ? status.reblog!.reblogged ?? false : status.reblogged ?? false
|
||||
|
||||
let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0)
|
||||
let incrLike: Int = isLiked ? 1 : 0
|
||||
print("original: \(status.favouritesCount)\nmin1: \(likeCount)\nincr1: \(likeCount + incrLike)")
|
||||
// let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0)
|
||||
// let incrLike: Int = isLiked ? 1 : 0
|
||||
// print("original: \(status.favouritesCount)\nmin1: \(likeCount)\nincr1: \(likeCount + incrLike)")
|
||||
}
|
||||
.task {
|
||||
await loadEmbeddedStatus()
|
||||
@ -81,7 +88,7 @@ struct CompactPostView: View {
|
||||
func statusPost(_ status: AnyStatus) -> some View {
|
||||
HStack(alignment: .top, spacing: 0) {
|
||||
// MARK: Profile picture
|
||||
if status.repliesCount > 0 {
|
||||
if status.repliesCount > 0 && preferences.experimental.replySymbol {
|
||||
VStack {
|
||||
profilePicture
|
||||
.onTapGesture {
|
||||
@ -130,10 +137,26 @@ struct CompactPostView: View {
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
if status.card != nil {
|
||||
if status.card != nil && status.mediaAttachments.isEmpty {
|
||||
PostCardView(card: status.card!)
|
||||
}
|
||||
|
||||
if !status.mediaAttachments.isEmpty {
|
||||
if status.mediaAttachments.count > 1 {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(alignment: .firstTextBaseline, spacing: 5) {
|
||||
ForEach(status.mediaAttachments) { attachment in
|
||||
PostAttachment(attachment: attachment)
|
||||
}
|
||||
}
|
||||
}
|
||||
.scrollIndicators(.hidden)
|
||||
.scrollClipDisabled()
|
||||
} else {
|
||||
PostAttachment(attachment: status.mediaAttachments.first!)
|
||||
}
|
||||
}
|
||||
|
||||
// if hasQuote {
|
||||
// if quoteStatus != nil {
|
||||
// QuotePostView(status: quoteStatus!)
|
||||
@ -328,14 +351,3 @@ struct CompactPostView: View {
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ScrollView {
|
||||
VStack {
|
||||
ForEach(Status.placeholders()) { status in
|
||||
CompactPostView(status: status, navigator: Navigator())
|
||||
.environment(Client.init(server: AppInfo.defaultServer))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,12 +2,17 @@
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ListStyle: View {
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
extension View {
|
||||
func listThreaded() -> some View {
|
||||
self
|
||||
.scrollContentBackground(.hidden)
|
||||
.tint(Color.white)
|
||||
.background(Color.appBackground)
|
||||
.listStyle(.inset)
|
||||
}
|
||||
func listRowThreaded() -> some View {
|
||||
self
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ListStyle()
|
||||
}
|
||||
|
@ -6,7 +6,8 @@ import NukeUI
|
||||
|
||||
struct OnlineImage: View {
|
||||
var url: URL?
|
||||
var size: CGFloat = 500
|
||||
var size: CGFloat? = 500
|
||||
var maxSize: CGFloat? = 500
|
||||
var priority: ImageRequest.Priority = .normal
|
||||
var useNuke: Bool = true
|
||||
|
||||
@ -18,6 +19,7 @@ struct OnlineImage: View {
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.aspectRatio(1.0, contentMode: .fit)
|
||||
.frame(idealWidth: size, maxWidth: maxSize)
|
||||
} else if state.error != nil {
|
||||
ContentUnavailableView("error.loading-image", systemImage: "rectangle.slash")
|
||||
} else {
|
||||
@ -30,14 +32,14 @@ struct OnlineImage: View {
|
||||
}
|
||||
}
|
||||
.priority(priority)
|
||||
.processors([.resize(width: size)])
|
||||
.processors([.resize(width: size ?? 500)])
|
||||
} else {
|
||||
AsyncImage(url: url) { element in
|
||||
element
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.aspectRatio(1.0, contentMode: .fit)
|
||||
.frame(width: size)
|
||||
.frame(minWidth: size, maxWidth: maxSize, alignment: .topLeading)
|
||||
} placeholder: {
|
||||
Rectangle()
|
||||
.fill(Color.gray)
|
||||
@ -72,6 +74,40 @@ struct OnlineImage: View {
|
||||
self.useNuke = true
|
||||
}
|
||||
|
||||
init(url: URL? = nil, maxSize: CGFloat, useNuke: Bool) {
|
||||
self.url = url
|
||||
self.maxSize = maxSize
|
||||
self.size = nil
|
||||
self.priority = .normal
|
||||
self.useNuke = useNuke
|
||||
}
|
||||
|
||||
/// Creates a new OnlineImage using Nuke, using the selected priority
|
||||
init(url: URL? = nil, maxSize: CGFloat, priority: ImageRequest.Priority) {
|
||||
self.url = url
|
||||
self.maxSize = maxSize
|
||||
self.size = nil
|
||||
self.priority = priority
|
||||
self.useNuke = true
|
||||
}
|
||||
|
||||
init(url: URL? = nil, size: CGFloat, maxSize: CGFloat, useNuke: Bool) {
|
||||
self.url = url
|
||||
self.maxSize = maxSize
|
||||
self.size = size
|
||||
self.priority = .normal
|
||||
self.useNuke = useNuke
|
||||
}
|
||||
|
||||
/// Creates a new OnlineImage using Nuke, using the selected priority
|
||||
init(url: URL? = nil, size: CGFloat, maxSize: CGFloat, priority: ImageRequest.Priority) {
|
||||
self.url = url
|
||||
self.maxSize = maxSize
|
||||
self.size = size
|
||||
self.priority = priority
|
||||
self.useNuke = true
|
||||
}
|
||||
|
||||
/// Change the priority of the Nuke OnlineImage
|
||||
mutating func setPriority(_ priority: ImageRequest.Priority) {
|
||||
guard self.useNuke == true else { return }
|
||||
|
@ -1,13 +1,158 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import SwiftUI
|
||||
import UIKit
|
||||
import AVKit
|
||||
|
||||
struct PostAttachment: View {
|
||||
@Environment(AppDelegate.self) private var appDelegate: AppDelegate
|
||||
var attachment: MediaAttachment
|
||||
@State private var player: AVPlayer?
|
||||
|
||||
var appLayoutWidth: CGFloat = 10
|
||||
var availableWidth: CGFloat {
|
||||
appDelegate.windowWidth * 0.8
|
||||
}
|
||||
var availableHeight: CGFloat {
|
||||
appDelegate.windowHeight
|
||||
}
|
||||
private let imageMaxHeight: CGFloat = 300
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
let mediaSize: CGSize = size(for: attachment) ?? .init(width: imageMaxHeight, height: imageMaxHeight)
|
||||
let newSize = imageSize(from: mediaSize)
|
||||
|
||||
GeometryReader { _ in
|
||||
// Audio later because it's a lil harder
|
||||
if attachment.supportedType == .image {
|
||||
if let url = attachment.url {
|
||||
AsyncImage(url: url) { image in
|
||||
image
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fill)
|
||||
.frame(width: newSize.width, height: newSize.height)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(.gray.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
} placeholder: {
|
||||
ZStack(alignment: .center) {
|
||||
Color.gray
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if attachment.supportedType == .gifv {
|
||||
ZStack(alignment: .center) {
|
||||
if player != nil {
|
||||
NoControlsPlayerViewController(player: player!)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(.gray.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
} else {
|
||||
Color.gray
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let url = attachment.url {
|
||||
player = AVPlayer(url: url)
|
||||
player?.audiovisualBackgroundPlaybackPolicy = .pauses
|
||||
player?.isMuted = true
|
||||
player?.play()
|
||||
|
||||
guard let player else { return }
|
||||
NotificationCenter.default.addObserver(forName: .AVPlayerItemDidPlayToEndTime, object: player.currentItem, queue: .main) { _ in
|
||||
Task { @MainActor in
|
||||
player.seek(to: CMTime.zero)
|
||||
player.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} else if attachment.supportedType == .video {
|
||||
ZStack {
|
||||
if player != nil {
|
||||
VideoPlayer(player: player)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(.gray.opacity(0.3), lineWidth: 1)
|
||||
)
|
||||
} else {
|
||||
Color.gray
|
||||
|
||||
ProgressView()
|
||||
.progressViewStyle(.circular)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if let url = attachment.url {
|
||||
player = AVPlayer(url: url)
|
||||
player?.audiovisualBackgroundPlaybackPolicy = .pauses
|
||||
player?.isMuted = false
|
||||
player?.play()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: newSize.width, height: newSize.height)
|
||||
.clipped()
|
||||
.clipShape(.rect(cornerRadius: 15))
|
||||
.contentShape(Rectangle())
|
||||
}
|
||||
|
||||
private func size(for media: MediaAttachment) -> CGSize? {
|
||||
guard let width = media.meta?.original?.width,
|
||||
let height = media.meta?.original?.height
|
||||
else { return nil }
|
||||
|
||||
return .init(width: CGFloat(width), height: CGFloat(height))
|
||||
}
|
||||
|
||||
private func imageSize(from: CGSize) -> CGSize {
|
||||
let boxWidth = availableWidth - appLayoutWidth
|
||||
let boxHeight = availableHeight * 0.8 // use only 80% of window height to leave room for text
|
||||
|
||||
if from.width <= boxWidth, from.height <= boxHeight {
|
||||
// intrinsic size of image fits just fine
|
||||
return from
|
||||
}
|
||||
|
||||
// shrink image proportionally to fit inside the box
|
||||
let xRatio = boxWidth / from.width
|
||||
let yRatio = boxHeight / from.height
|
||||
if xRatio < yRatio {
|
||||
return .init(width: boxWidth, height: from.height * xRatio)
|
||||
} else {
|
||||
return .init(width: from.width * yRatio, height: boxHeight)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PostAttachment()
|
||||
class NoControlsAVPlayerViewController: AVPlayerViewController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.showsPlaybackControls = false
|
||||
}
|
||||
}
|
||||
|
||||
// Create a UIViewRepresentable for the customized AVPlayerViewController
|
||||
struct NoControlsPlayerViewController: UIViewControllerRepresentable {
|
||||
let player: AVPlayer
|
||||
|
||||
func updateUIViewController(_ uiViewController: NoControlsAVPlayerViewController, context: Context) {
|
||||
// update
|
||||
}
|
||||
|
||||
func makeUIViewController(context: Context) -> NoControlsAVPlayerViewController {
|
||||
let customPlayerVC = NoControlsAVPlayerViewController()
|
||||
customPlayerVC.player = player // Set the AVPlayer
|
||||
return customPlayerVC
|
||||
}
|
||||
}
|
||||
|
@ -13,7 +13,6 @@ struct QuotePostView: View {
|
||||
.frame(width: 250)
|
||||
.padding(.horizontal, 10)
|
||||
.clipShape(.rect(cornerRadius: 15))
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 15)
|
||||
.stroke(.gray.opacity(0.3), lineWidth: 1)
|
||||
|
@ -68,6 +68,7 @@ public enum SheetDestination: Identifiable {
|
||||
public enum RouterDestination: Hashable {
|
||||
case settings
|
||||
case privacy
|
||||
case appearence
|
||||
case account(acc: Account)
|
||||
case about
|
||||
}
|
||||
@ -80,6 +81,8 @@ extension View {
|
||||
SettingsView(navigator: navigator)
|
||||
case .privacy:
|
||||
PrivacyView()
|
||||
case .appearence:
|
||||
AppearenceView()
|
||||
case .account(let acc):
|
||||
AccountView(account: acc, navigator: navigator)
|
||||
case .about:
|
||||
|
@ -1,3 +1,76 @@
|
||||
//Made by Lumaa
|
||||
|
||||
import Foundation
|
||||
|
||||
@Observable
|
||||
class UserPreferences: Codable, ObservableObject {
|
||||
private static let saveKey: String = "threaded-preferences.user"
|
||||
public static let defaultPreferences: UserPreferences = .init()
|
||||
|
||||
// Final
|
||||
var displayedName: DisplayedName = .username
|
||||
var profilePictureShape: ProfilePictureShape = .circle
|
||||
|
||||
// Experimental
|
||||
var showExperimental: Bool = false
|
||||
var experimental: UserPreferences.Experimental
|
||||
|
||||
init(displayedName: DisplayedName = .username, profilePictureShape: ProfilePictureShape = .circle, showExperimental: Bool = false, experimental: UserPreferences.Experimental = .init()) {
|
||||
self.displayedName = displayedName
|
||||
self.profilePictureShape = profilePictureShape
|
||||
|
||||
self.showExperimental = showExperimental
|
||||
self.experimental = experimental
|
||||
}
|
||||
|
||||
@Observable
|
||||
class Experimental: Codable, ObservableObject {
|
||||
private static let saveKey: String = "threaded-preferences.experimental"
|
||||
|
||||
var replySymbol: Bool = false
|
||||
|
||||
init(replySymbol: Bool = false) {
|
||||
self.replySymbol = replySymbol
|
||||
}
|
||||
|
||||
func saveAsCurrent() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let json = try encoder.encode(self)
|
||||
UserDefaults.standard.setValue(json, forKey: UserPreferences.Experimental.saveKey)
|
||||
}
|
||||
|
||||
static func loadAsCurrent() throws -> UserPreferences.Experimental? {
|
||||
let decoder = JSONDecoder()
|
||||
if let data = UserDefaults.standard.data(forKey: UserPreferences.Experimental.saveKey) {
|
||||
let exp = try decoder.decode(UserPreferences.Experimental.self, from: data)
|
||||
return exp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func saveAsCurrent() throws {
|
||||
let encoder = JSONEncoder()
|
||||
let json = try encoder.encode(self)
|
||||
UserDefaults.standard.setValue(json, forKey: UserPreferences.saveKey)
|
||||
}
|
||||
|
||||
static func loadAsCurrent() throws -> UserPreferences? {
|
||||
let decoder = JSONDecoder()
|
||||
if let data = UserDefaults.standard.data(forKey: UserPreferences.saveKey) {
|
||||
let pref = try decoder.decode(UserPreferences.self, from: data)
|
||||
return pref
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Enums and other
|
||||
|
||||
enum DisplayedName: Codable, CaseIterable {
|
||||
case username, displayName, both
|
||||
}
|
||||
|
||||
enum ProfilePictureShape: Codable, CaseIterable {
|
||||
case circle, rounded
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,7 @@
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Threaded is a very simple Mastodon client, that is meant to look like the newest social media Threads made by Meta Platforms. It integrates perfectly with your Mastodon account, and matches the Threads vibe, while having Mastodon-only features.\n\nThreaded is a 100% free, made in France using SwiftUI, [open-source](https://github.com/lumaa-dev/ThreadedApp), and doesn't violate [your privacy](https://apps.lumaa.fr/legal/privacy). [Learn more](https://apps.lumaa.fr/app/threaded)"
|
||||
"value" : "Threaded is a very simple Mastodon client, that is meant to look like the newest social media Threads made by Meta Platforms. It integrates perfectly with your Mastodon account, and matches the Threads vibe, while having Mastodon-only features.\n\nThreaded is a 100% free, made in France using SwiftUI, [open-source](https://github.com/lumaa-dev/ThreadedApp), and doesn't violate [your privacy](https://apps.lumaa.fr/legal/privacy).\n\nThreaded is not related or affiliated to Meta Platforms."
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -134,6 +134,16 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"experimental" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Experimental"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"Hello world" : {
|
||||
|
||||
},
|
||||
@ -246,6 +256,99 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Appearence"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.displayed-name" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Displayed Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.displayed-name.both" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Both"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.displayed-name.display-name" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Display Name"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.displayed-name.username" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Username"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.pfp-shape" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Shape of Profile Pictures"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.pfp-shape.circle" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Circle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.pfp-shape.rounded" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Rounded Rectangle"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.appearence.reply-symbols" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Show Reply Symbols"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"setting.experimental.activate" : {
|
||||
|
||||
},
|
||||
"setting.privacy" : {
|
||||
"localizations" : {
|
||||
@ -267,6 +370,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.cancel" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Cancel"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"settings.done" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "Done"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"status.favourites-%lld" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@ -458,4 +581,4 @@
|
||||
}
|
||||
},
|
||||
"version" : "1.0"
|
||||
}
|
||||
}
|
@ -3,6 +3,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct ContentView: View {
|
||||
@UIApplicationDelegateAdaptor private var appDelegate: AppDelegate
|
||||
|
||||
@State private var navigator = Navigator()
|
||||
@State private var sheet: SheetDestination?
|
||||
@State private var accountManager: AccountManager = AccountManager()
|
||||
@ -46,6 +48,7 @@ struct ContentView: View {
|
||||
.withSheets(sheetDestination: $navigator.presentedSheet)
|
||||
.environment(accountManager)
|
||||
.environment(navigator)
|
||||
.environment(appDelegate)
|
||||
.task {
|
||||
await recognizeAccount()
|
||||
}
|
||||
|
@ -3,11 +3,280 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PostDetailsView: View {
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
|
||||
var status: Status
|
||||
|
||||
@State private var initialLike: Bool = false
|
||||
@State private var isLiked: Bool = false
|
||||
@State private var isReposted: Bool = false
|
||||
@State private var hasQuote: Bool = false
|
||||
@State private var quoteStatus: Status? = nil
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
VStack {
|
||||
statusPost(status, isMain: true)
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func statusPost(_ status: AnyStatus, isMain: Bool = false) -> some View {
|
||||
VStack {
|
||||
HStack {
|
||||
profilePicture
|
||||
.onTapGesture {
|
||||
navigator.navigate(to: .account(acc: status.account))
|
||||
}
|
||||
|
||||
Text(status.account.username)
|
||||
.multilineTextAlignment(.leading)
|
||||
.bold()
|
||||
.onTapGesture {
|
||||
navigator.navigate(to: .account(acc: status.account))
|
||||
}
|
||||
}
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
// MARK: Status main content
|
||||
VStack(alignment: .leading, spacing: 10) {
|
||||
if !status.content.asRawText.isEmpty {
|
||||
TextEmoji(status.content, emojis: status.emojis, language: status.language)
|
||||
.multilineTextAlignment(.leading)
|
||||
.frame(width: 300, alignment: .topLeading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.font(.callout)
|
||||
}
|
||||
|
||||
if status.card != nil && status.mediaAttachments.isEmpty {
|
||||
PostCardView(card: status.card!)
|
||||
}
|
||||
|
||||
if !status.mediaAttachments.isEmpty {
|
||||
ForEach(status.mediaAttachments) { attachment in
|
||||
PostAttachment(attachment: attachment)
|
||||
}
|
||||
}
|
||||
|
||||
// if hasQuote {
|
||||
// if quoteStatus != nil {
|
||||
// QuotePostView(status: quoteStatus!)
|
||||
// } else {
|
||||
// ProgressView()
|
||||
// .progressViewStyle(.circular)
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
//MARK: Action buttons
|
||||
HStack(spacing: 13) {
|
||||
asyncActionButton(isLiked ? "heart.fill" : "heart") {
|
||||
do {
|
||||
try await likePost()
|
||||
HapticManager.playHaptics(haptics: Haptic.tap)
|
||||
} catch {
|
||||
HapticManager.playHaptics(haptics: Haptic.error)
|
||||
print("Error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
actionButton("bubble.right") {
|
||||
print("reply")
|
||||
navigator.presentedSheet = .post()
|
||||
}
|
||||
asyncActionButton(isReposted ? "bolt.horizontal.fill" : "bolt.horizontal") {
|
||||
do {
|
||||
try await repostPost()
|
||||
HapticManager.playHaptics(haptics: Haptic.tap)
|
||||
} catch {
|
||||
HapticManager.playHaptics(haptics: Haptic.error)
|
||||
print("Error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
ShareLink(item: URL(string: status.url ?? "https://joinmastodon.org/")!) {
|
||||
Image(systemName: "square.and.arrow.up")
|
||||
.font(.title2)
|
||||
}
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
.padding(.top)
|
||||
|
||||
// MARK: Status stats
|
||||
stats.padding(.top, 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func likePost() async throws {
|
||||
if let client = accountManager.getClient() {
|
||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
||||
let endpoint = !isLiked ? Statuses.favorite(id: statusId) : Statuses.unfavorite(id: statusId)
|
||||
|
||||
isLiked = !isLiked
|
||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||
if isLiked != newStatus.favourited {
|
||||
isLiked = newStatus.favourited ?? !isLiked
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func repostPost() async throws {
|
||||
if let client = accountManager.getClient() {
|
||||
guard client.isAuth else { fatalError("Client is not authenticated") }
|
||||
let statusId: String = status.reblog != nil ? status.reblog!.id : status.id
|
||||
let endpoint = !isReposted ? Statuses.reblog(id: statusId) : Statuses.unreblog(id: statusId)
|
||||
|
||||
isReposted = !isReposted
|
||||
let newStatus: Status = try await client.post(endpoint: endpoint)
|
||||
if isReposted != newStatus.reblogged {
|
||||
isReposted = newStatus.reblogged ?? !isReposted
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var pinnedNotice: some View {
|
||||
HStack (alignment:.center, spacing: 5) {
|
||||
Image(systemName: "pin.fill")
|
||||
|
||||
Text("status.pinned")
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color(uiColor: UIColor.label).opacity(0.3))
|
||||
}
|
||||
|
||||
var repostNotice: some View {
|
||||
HStack (alignment:.center, spacing: 5) {
|
||||
Image(systemName: "bolt.horizontal")
|
||||
|
||||
Text("status.reposted-by.\(status.account.username)")
|
||||
}
|
||||
.padding(.leading, 20)
|
||||
.multilineTextAlignment(.leading)
|
||||
.lineLimit(1)
|
||||
.font(.caption)
|
||||
.foregroundStyle(Color(uiColor: UIColor.label).opacity(0.3))
|
||||
}
|
||||
|
||||
var profilePicture: some View {
|
||||
if status.reblog != nil {
|
||||
OnlineImage(url: status.reblog!.account.avatar, size: 50, useNuke: true)
|
||||
.frame(width: 40, height: 40)
|
||||
.padding(.horizontal)
|
||||
.clipShape(.circle)
|
||||
} else {
|
||||
OnlineImage(url: status.account.avatar, size: 50, useNuke: true)
|
||||
.frame(width: 40, height: 40)
|
||||
.padding(.horizontal)
|
||||
.clipShape(.circle)
|
||||
}
|
||||
}
|
||||
|
||||
var stats: some View {
|
||||
//MARK: I acknowledge the existance of a count bug here
|
||||
if status.reblog == nil {
|
||||
HStack {
|
||||
if status.repliesCount > 0 {
|
||||
Text("status.replies-\(status.repliesCount)")
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
|
||||
if status.repliesCount > 0 && (status.favouritesCount > 0 || isLiked) {
|
||||
Text("•")
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
|
||||
if status.favouritesCount > 0 || isLiked {
|
||||
let likeCount: Int = status.favouritesCount - (initialLike ? 1 : 0)
|
||||
let incrLike: Int = isLiked ? 1 : 0
|
||||
Text("status.favourites-\(likeCount + incrLike)")
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.gray)
|
||||
.contentTransition(.numericText(value: Double(likeCount + incrLike)))
|
||||
.transaction { t in
|
||||
t.animation = .default
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
HStack {
|
||||
if status.reblog!.repliesCount > 0 {
|
||||
Text("status.replies-\(status.reblog!.repliesCount)")
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
|
||||
if status.reblog!.repliesCount > 0 && (status.reblog!.favouritesCount > 0 || isLiked) {
|
||||
Text("•")
|
||||
.foregroundStyle(.gray)
|
||||
}
|
||||
|
||||
if status.reblog!.favouritesCount > 0 || isLiked {
|
||||
let likeCount: Int = status.reblog!.favouritesCount - (initialLike ? 1 : 0)
|
||||
let incrLike: Int = isLiked ? 1 : 0
|
||||
Text("status.favourites-\(likeCount + incrLike)")
|
||||
.monospacedDigit()
|
||||
.foregroundStyle(.gray)
|
||||
.contentTransition(.numericText(value: Double(likeCount + incrLike)))
|
||||
.transaction { t in
|
||||
t.animation = .default
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func embededStatusURL() -> URL? {
|
||||
let content = status.content
|
||||
if let client = accountManager.getClient() {
|
||||
if !content.statusesURLs.isEmpty, let url = content.statusesURLs.first, client.hasConnection(with: url) {
|
||||
return url
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadEmbeddedStatus() async {
|
||||
guard let url = embededStatusURL(), let client = accountManager.getClient() else { hasQuote = false; return }
|
||||
|
||||
do {
|
||||
hasQuote = true
|
||||
if url.absoluteString.contains(client.server), let id = Int(url.lastPathComponent) {
|
||||
quoteStatus = try await client.get(endpoint: Statuses.status(id: String(id)))
|
||||
} else {
|
||||
let results: SearchResults = try await client.get(endpoint: Search.search(query: url.absoluteString, type: "statuses", offset: 0, following: nil), forceVersion: .v2)
|
||||
quoteStatus = results.statuses.first
|
||||
}
|
||||
} catch {
|
||||
hasQuote = false
|
||||
quoteStatus = nil
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func actionButton(_ image: String, action: @escaping () -> Void) -> some View {
|
||||
Button {
|
||||
action()
|
||||
} label: {
|
||||
Image(systemName: image)
|
||||
.font(.title2)
|
||||
}
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
func asyncActionButton(_ image: String, action: @escaping () async -> Void) -> some View {
|
||||
Button {
|
||||
Task {
|
||||
await action()
|
||||
}
|
||||
} label: {
|
||||
Image(systemName: image)
|
||||
.font(.title2)
|
||||
}
|
||||
.tint(Color(uiColor: UIColor.label))
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PostDetailsView()
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import PhotosUI
|
||||
struct PostingView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
@Environment(AccountManager.self) private var accountManager: AccountManager
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
|
||||
var initialString: String = ""
|
||||
@State private var postText: NSMutableAttributedString = .init(string: "")
|
||||
@ -87,9 +88,10 @@ struct PostingView: View {
|
||||
Task {
|
||||
if let client = accountManager.getClient() {
|
||||
postingStatus = true
|
||||
try await client.post(endpoint: Statuses.postStatus(json: .init(status: postText.string, visibility: visibility)))
|
||||
let postedStatus: Status = try await client.post(endpoint: Statuses.postStatus(json: .init(status: postText.string, visibility: visibility)))
|
||||
postingStatus = false
|
||||
dismiss()
|
||||
// navigate to post
|
||||
}
|
||||
}
|
||||
} label: {
|
||||
|
@ -3,6 +3,8 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AboutView: View {
|
||||
@ObservedObject private var userPreferences: UserPreferences = .defaultPreferences
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
NavigationLink {
|
||||
@ -11,14 +13,33 @@ struct AboutView: View {
|
||||
Text("about.app")
|
||||
.tint(Color.blue)
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
.listRowThreaded()
|
||||
|
||||
Toggle("setting.experimental.activate", isOn: $userPreferences.showExperimental)
|
||||
.listRowThreaded()
|
||||
.onAppear {
|
||||
do {
|
||||
let oldPreferences = try UserPreferences.loadAsCurrent() ?? UserPreferences.defaultPreferences
|
||||
|
||||
userPreferences.showExperimental = oldPreferences.showExperimental
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
.listStyle(.plain)
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackground)
|
||||
.listThreaded()
|
||||
.navigationTitle("about")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onDisappear {
|
||||
do {
|
||||
if !userPreferences.showExperimental {
|
||||
userPreferences.experimental = .init()
|
||||
}
|
||||
try userPreferences.saveAsCurrent()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var aboutApp: some View {
|
||||
@ -31,8 +52,7 @@ struct AboutView: View {
|
||||
}
|
||||
.padding(.horizontal)
|
||||
}
|
||||
.scrollContentBackground(.hidden)
|
||||
.background(Color.appBackground)
|
||||
.listThreaded()
|
||||
.navigationTitle("about.app")
|
||||
.navigationBarTitleDisplayMode(.large)
|
||||
}
|
||||
|
@ -3,8 +3,103 @@
|
||||
import SwiftUI
|
||||
|
||||
struct AppearenceView: View {
|
||||
@ObservedObject private var userPreferences: UserPreferences = .defaultPreferences
|
||||
@Environment(Navigator.self) private var navigator: Navigator
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/)
|
||||
List {
|
||||
Picker(LocalizedStringKey("setting.appearence.displayed-name"), selection: $userPreferences.displayedName) {
|
||||
ForEach(UserPreferences.DisplayedName.allCases, id: \.self) { displayCase in
|
||||
switch (displayCase) {
|
||||
case .username:
|
||||
Text("setting.appearence.displayed-name.username")
|
||||
.tag(UserPreferences.DisplayedName.username)
|
||||
case .displayName:
|
||||
Text("setting.appearence.displayed-name.display-name")
|
||||
.tag(UserPreferences.DisplayedName.displayName)
|
||||
case .both:
|
||||
Text("setting.appearence.displayed-name.both")
|
||||
.tag(UserPreferences.DisplayedName.both)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
.listRowThreaded()
|
||||
|
||||
Picker(LocalizedStringKey("setting.appearence.pfp-shape"), selection: $userPreferences.profilePictureShape) {
|
||||
ForEach(UserPreferences.ProfilePictureShape.allCases, id: \.self) { displayCase in
|
||||
switch (displayCase) {
|
||||
case .circle:
|
||||
Text("setting.appearence.pfp-shape.circle")
|
||||
.tag(UserPreferences.ProfilePictureShape.circle)
|
||||
case .rounded:
|
||||
Text("setting.appearence.pfp-shape.rounded")
|
||||
.tag(UserPreferences.ProfilePictureShape.rounded)
|
||||
}
|
||||
}
|
||||
}
|
||||
.pickerStyle(.inline)
|
||||
.listRowThreaded()
|
||||
|
||||
if userPreferences.showExperimental {
|
||||
Section(header: Text("experimental")) {
|
||||
Toggle(LocalizedStringKey("setting.appearence.reply-symbols"), isOn: $userPreferences.experimental.replySymbol)
|
||||
.listRowThreaded()
|
||||
}
|
||||
}
|
||||
}
|
||||
.listThreaded()
|
||||
.navigationTitle("setting.appearence")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.navigationBarBackButtonHidden()
|
||||
.onAppear {
|
||||
do {
|
||||
let oldPreferences = try UserPreferences.loadAsCurrent() ?? UserPreferences.defaultPreferences
|
||||
|
||||
userPreferences.displayedName = oldPreferences.displayedName
|
||||
userPreferences.profilePictureShape = oldPreferences.profilePictureShape
|
||||
|
||||
userPreferences.experimental.replySymbol = oldPreferences.experimental.replySymbol
|
||||
} catch {
|
||||
print(error)
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button {
|
||||
do {
|
||||
let oldPreferences = try UserPreferences.loadAsCurrent() ?? UserPreferences.defaultPreferences
|
||||
|
||||
userPreferences.displayedName = oldPreferences.displayedName
|
||||
userPreferences.profilePictureShape = oldPreferences.profilePictureShape
|
||||
|
||||
userPreferences.experimental.replySymbol = oldPreferences.experimental.replySymbol
|
||||
|
||||
dismiss()
|
||||
} catch {
|
||||
print(error)
|
||||
dismiss()
|
||||
}
|
||||
} label: {
|
||||
Text("settings.cancel")
|
||||
}
|
||||
}
|
||||
|
||||
ToolbarItem(placement: .primaryAction) {
|
||||
Button {
|
||||
do {
|
||||
try userPreferences.saveAsCurrent()
|
||||
dismiss()
|
||||
} catch {
|
||||
print(error)
|
||||
}
|
||||
} label: {
|
||||
Text("settings.done")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,39 +9,42 @@ struct SettingsView: View {
|
||||
var body: some View {
|
||||
NavigationStack(path: $navigator.path) {
|
||||
List {
|
||||
Button {
|
||||
navigator.navigate(to: .about)
|
||||
} label: {
|
||||
Label("about", systemImage: "info.circle")
|
||||
Section {
|
||||
Button {
|
||||
navigator.navigate(to: .about)
|
||||
} label: {
|
||||
Label("about", systemImage: "info.circle")
|
||||
}
|
||||
.listRowThreaded()
|
||||
|
||||
Button {
|
||||
navigator.navigate(to: .privacy)
|
||||
} label: {
|
||||
Label("setting.privacy", systemImage: "lock")
|
||||
}
|
||||
.listRowThreaded()
|
||||
|
||||
Button {
|
||||
navigator.navigate(to: .appearence)
|
||||
} label: {
|
||||
Label("setting.appearence", systemImage: "rectangle.3.group")
|
||||
}
|
||||
.listRowThreaded()
|
||||
|
||||
Button {
|
||||
AppAccount.clear()
|
||||
sheet = .welcome
|
||||
} label: {
|
||||
Text("logout")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.tint(Color.red)
|
||||
.listRowThreaded()
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
|
||||
Button {
|
||||
navigator.navigate(to: .privacy)
|
||||
} label: {
|
||||
Label("setting.privacy", systemImage: "lock")
|
||||
}
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
|
||||
Button {
|
||||
AppAccount.clear()
|
||||
sheet = .welcome
|
||||
} label: {
|
||||
Text("logout")
|
||||
.foregroundStyle(.red)
|
||||
}
|
||||
.tint(Color.red)
|
||||
.listRowSeparator(.hidden)
|
||||
.listRowBackground(Color.appBackground)
|
||||
}
|
||||
.withAppRouter(navigator)
|
||||
.withCovers(sheetDestination: $sheet)
|
||||
.scrollContentBackground(.hidden)
|
||||
.tint(Color.white)
|
||||
.background(Color.appBackground)
|
||||
.listStyle(.inset)
|
||||
.listThreaded()
|
||||
.navigationTitle("settings")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user