View refactoring
This commit is contained in:
parent
cc73acedd0
commit
271b4f4e0f
|
@ -15,8 +15,20 @@
|
|||
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F80048092961EA1900E6868A /* AttachmentDataHandler.swift */; };
|
||||
F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage.swift */; };
|
||||
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; };
|
||||
F83901A4295D864D00456AE2 /* TagView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A3295D864D00456AE2 /* TagView.swift */; };
|
||||
F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIconView.swift */; };
|
||||
F83901A4295D864D00456AE2 /* Tag.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A3295D864D00456AE2 /* Tag.swift */; };
|
||||
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */ = {isa = PBXBuildFile; fileRef = F83901A5295D8EC000456AE2 /* LabelIcon.swift */; };
|
||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4970296402DC00751DF7 /* AuthorizationService.swift */; };
|
||||
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4972296406E700751DF7 /* BottomRight.swift */; };
|
||||
F85D4975296407F100751DF7 /* TimelineService.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4974296407F100751DF7 /* TimelineService.swift */; };
|
||||
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497629640A5200751DF7 /* ImageRow.swift */; };
|
||||
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497829640B9D00751DF7 /* ImagesCarousel.swift */; };
|
||||
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497A29640C8200751DF7 /* UsernameRow.swift */; };
|
||||
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497C29640D5900751DF7 /* InteractionRow.swift */; };
|
||||
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D497E296416C800751DF7 /* CommentsSection.swift */; };
|
||||
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */; };
|
||||
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */; };
|
||||
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49842964301800751DF7 /* StatusData+Attachments.swift */; };
|
||||
F85D49872964334100751DF7 /* String+Date.swift in Sources */ = {isa = PBXBuildFile; fileRef = F85D49862964334100751DF7 /* String+Date.swift */; };
|
||||
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */; };
|
||||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */; };
|
||||
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = F866F6A229604161002E8F88 /* AccountDataHandler.swift */; };
|
||||
|
@ -52,8 +64,20 @@
|
|||
F80048092961EA1900E6868A /* AttachmentDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AttachmentDataHandler.swift; sourceTree = "<group>"; };
|
||||
F8341F8F295C636C009C8EE6 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = "<group>"; };
|
||||
F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = "<group>"; };
|
||||
F83901A3295D864D00456AE2 /* TagView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TagView.swift; sourceTree = "<group>"; };
|
||||
F83901A5295D8EC000456AE2 /* LabelIconView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIconView.swift; sourceTree = "<group>"; };
|
||||
F83901A3295D864D00456AE2 /* Tag.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Tag.swift; sourceTree = "<group>"; };
|
||||
F83901A5295D8EC000456AE2 /* LabelIcon.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LabelIcon.swift; sourceTree = "<group>"; };
|
||||
F85D4970296402DC00751DF7 /* AuthorizationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AuthorizationService.swift; sourceTree = "<group>"; };
|
||||
F85D4972296406E700751DF7 /* BottomRight.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BottomRight.swift; sourceTree = "<group>"; };
|
||||
F85D4974296407F100751DF7 /* TimelineService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TimelineService.swift; sourceTree = "<group>"; };
|
||||
F85D497629640A5200751DF7 /* ImageRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageRow.swift; sourceTree = "<group>"; };
|
||||
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImagesCarousel.swift; sourceTree = "<group>"; };
|
||||
F85D497A29640C8200751DF7 /* UsernameRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UsernameRow.swift; sourceTree = "<group>"; };
|
||||
F85D497C29640D5900751DF7 /* InteractionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InteractionRow.swift; sourceTree = "<group>"; };
|
||||
F85D497E296416C800751DF7 /* CommentsSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CommentsSection.swift; sourceTree = "<group>"; };
|
||||
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "MastodonClientAuthenticated+Context.swift"; sourceTree = "<group>"; };
|
||||
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "AttachmentData+Comperable.swift"; sourceTree = "<group>"; };
|
||||
F85D49842964301800751DF7 /* StatusData+Attachments.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "StatusData+Attachments.swift"; sourceTree = "<group>"; };
|
||||
F85D49862964334100751DF7 /* String+Date.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "String+Date.swift"; sourceTree = "<group>"; };
|
||||
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "ApplicationSettings+CoreDataProperties.swift"; sourceTree = "<group>"; };
|
||||
F866F6A229604161002E8F88 /* AccountDataHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccountDataHandler.swift; sourceTree = "<group>"; };
|
||||
|
@ -111,6 +135,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
F8341F8F295C636C009C8EE6 /* UIImage.swift */,
|
||||
F85D4980296417F700751DF7 /* MastodonClientAuthenticated+Context.swift */,
|
||||
F85D49862964334100751DF7 /* String+Date.swift */,
|
||||
);
|
||||
path = Extensions;
|
||||
sourceTree = "<group>";
|
||||
|
@ -130,8 +156,10 @@
|
|||
children = (
|
||||
F80047FF2961850500E6868A /* AttachmentData+CoreDataClass.swift */,
|
||||
F80048002961850500E6868A /* AttachmentData+CoreDataProperties.swift */,
|
||||
F85D498229642FAC00751DF7 /* AttachmentData+Comperable.swift */,
|
||||
F80048012961850500E6868A /* StatusData+CoreDataClass.swift */,
|
||||
F80048022961850500E6868A /* StatusData+CoreDataProperties.swift */,
|
||||
F85D49842964301800751DF7 /* StatusData+Attachments.swift */,
|
||||
F866F69E296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift */,
|
||||
F866F69F296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift */,
|
||||
F88FAD28295F43B8009B20C9 /* AccountData+CoreDataClass.swift */,
|
||||
|
@ -156,8 +184,14 @@
|
|||
F83901A2295D863B00456AE2 /* Widgets */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
F83901A3295D864D00456AE2 /* TagView.swift */,
|
||||
F83901A5295D8EC000456AE2 /* LabelIconView.swift */,
|
||||
F83901A3295D864D00456AE2 /* Tag.swift */,
|
||||
F83901A5295D8EC000456AE2 /* LabelIcon.swift */,
|
||||
F85D4972296406E700751DF7 /* BottomRight.swift */,
|
||||
F85D497629640A5200751DF7 /* ImageRow.swift */,
|
||||
F85D497829640B9D00751DF7 /* ImagesCarousel.swift */,
|
||||
F85D497A29640C8200751DF7 /* UsernameRow.swift */,
|
||||
F85D497C29640D5900751DF7 /* InteractionRow.swift */,
|
||||
F85D497E296416C800751DF7 /* CommentsSection.swift */,
|
||||
);
|
||||
path = Widgets;
|
||||
sourceTree = "<group>";
|
||||
|
@ -210,6 +244,8 @@
|
|||
isa = PBXGroup;
|
||||
children = (
|
||||
F88FAD31295F5029009B20C9 /* RemoteFileService.swift */,
|
||||
F85D4970296402DC00751DF7 /* AuthorizationService.swift */,
|
||||
F85D4974296407F100751DF7 /* TimelineService.swift */,
|
||||
);
|
||||
path = Services;
|
||||
sourceTree = "<group>";
|
||||
|
@ -290,30 +326,41 @@
|
|||
isa = PBXSourcesBuildPhase;
|
||||
buildActionMask = 2147483647;
|
||||
files = (
|
||||
F85D497729640A5200751DF7 /* ImageRow.swift in Sources */,
|
||||
F80048082961E6DE00E6868A /* StatusDataHandler.swift in Sources */,
|
||||
F866F6A0296040A8002E8F88 /* ApplicationSettings+CoreDataClass.swift in Sources */,
|
||||
F88FAD23295F3FC4009B20C9 /* LocalFeedView.swift in Sources */,
|
||||
F88FAD2B295F43B8009B20C9 /* AccountData+CoreDataProperties.swift in Sources */,
|
||||
F85D4975296407F100751DF7 /* TimelineService.swift in Sources */,
|
||||
F80048062961850500E6868A /* StatusData+CoreDataProperties.swift in Sources */,
|
||||
F88FAD21295F3944009B20C9 /* HomeFeedView.swift in Sources */,
|
||||
F88C2475295C37BB0006098B /* CoreDataHandler.swift in Sources */,
|
||||
F88FAD2A295F43B8009B20C9 /* AccountData+CoreDataClass.swift in Sources */,
|
||||
F85D49872964334100751DF7 /* String+Date.swift in Sources */,
|
||||
F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */,
|
||||
F85D498329642FAC00751DF7 /* AttachmentData+Comperable.swift in Sources */,
|
||||
F85D497B29640C8200751DF7 /* UsernameRow.swift in Sources */,
|
||||
F85D497929640B9D00751DF7 /* ImagesCarousel.swift in Sources */,
|
||||
F80048052961850500E6868A /* StatusData+CoreDataClass.swift in Sources */,
|
||||
F80048042961850500E6868A /* AttachmentData+CoreDataProperties.swift in Sources */,
|
||||
F83901A6295D8EC000456AE2 /* LabelIconView.swift in Sources */,
|
||||
F83901A6295D8EC000456AE2 /* LabelIcon.swift in Sources */,
|
||||
F800480A2961EA1900E6868A /* AttachmentDataHandler.swift in Sources */,
|
||||
F80048032961850500E6868A /* AttachmentData+CoreDataClass.swift in Sources */,
|
||||
F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */,
|
||||
F85D4981296417F700751DF7 /* MastodonClientAuthenticated+Context.swift in Sources */,
|
||||
F88C246E295C37B80006098B /* MainView.swift in Sources */,
|
||||
F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */,
|
||||
F88C2482295C3A4F0006098B /* DetailsView.swift in Sources */,
|
||||
F866F6A329604161002E8F88 /* AccountDataHandler.swift in Sources */,
|
||||
F85D497F296416C800751DF7 /* CommentsSection.swift in Sources */,
|
||||
F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */,
|
||||
F866F6A529604194002E8F88 /* ApplicationSettingsHandler.swift in Sources */,
|
||||
F85D49852964301800751DF7 /* StatusData+Attachments.swift in Sources */,
|
||||
F85D497D29640D5900751DF7 /* InteractionRow.swift in Sources */,
|
||||
F866F6A729604629002E8F88 /* SignInView.swift in Sources */,
|
||||
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */,
|
||||
F83901A4295D864D00456AE2 /* TagView.swift in Sources */,
|
||||
F85D4971296402DC00751DF7 /* AuthorizationService.swift in Sources */,
|
||||
F83901A4295D864D00456AE2 /* Tag.swift in Sources */,
|
||||
F88FAD25295F3FF7009B20C9 /* FederatedFeedView.swift in Sources */,
|
||||
F88FAD32295F5029009B20C9 /* RemoteFileService.swift in Sources */,
|
||||
F88FAD27295F400E009B20C9 /* NotificationsView.swift in Sources */,
|
||||
|
@ -321,6 +368,7 @@
|
|||
F866F6A1296040A8002E8F88 /* ApplicationSettings+CoreDataProperties.swift in Sources */,
|
||||
F866F6AE29606367002E8F88 /* ApplicationViewMode.swift in Sources */,
|
||||
F866F6AA29605AFA002E8F88 /* SceneDelegate.swift in Sources */,
|
||||
F85D4973296406E700751DF7 /* BottomRight.swift in Sources */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,14 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import Foundation
|
||||
|
||||
extension AttachmentData : Comparable {
|
||||
public static func < (lhs: AttachmentData, rhs: AttachmentData) -> Bool {
|
||||
lhs.id < rhs.id
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension StatusData {
|
||||
func attachments() -> [AttachmentData] {
|
||||
return self.attachmentRelation?.sorted(by: <) ?? []
|
||||
}
|
||||
}
|
|
@ -62,9 +62,3 @@ extension StatusData {
|
|||
extension StatusData : Identifiable {
|
||||
|
||||
}
|
||||
|
||||
extension StatusData {
|
||||
func attachments() -> [AttachmentData] {
|
||||
return Array(self.attachmentRelation ?? [])
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSwift
|
||||
|
||||
extension MastodonClientAuthenticated {
|
||||
func getContext(for statusId: String) async throws -> Context {
|
||||
let request = try Self.request(
|
||||
for: baseURL,
|
||||
target: Mastodon.Statuses.context(statusId),
|
||||
withBearerToken: token
|
||||
)
|
||||
|
||||
let (data, _) = try await urlSession.data(for: request)
|
||||
return try JSONDecoder().decode(Context.self, from: data)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
public enum DateFormatType {
|
||||
/// The ISO8601 formatted year "yyyy" i.e. 1997
|
||||
case isoYear
|
||||
|
||||
/// The ISO8601 formatted year and month "yyyy-MM" i.e. 1997-07
|
||||
case isoYearMonth
|
||||
|
||||
/// The ISO8601 formatted date "yyyy-MM-dd" i.e. 1997-07-16
|
||||
case isoDate
|
||||
|
||||
/// The ISO8601 formatted date and time "yyyy-MM-dd'T'HH:mmZ" i.e. 1997-07-16T19:20+01:00
|
||||
case isoDateTime
|
||||
|
||||
/// The ISO8601 formatted date, time and sec "yyyy-MM-dd'T'HH:mm:ssZ" i.e. 1997-07-16T19:20:30+01:00
|
||||
case isoDateTimeSec
|
||||
|
||||
/// The ISO8601 formatted date, time and millisec "yyyy-MM-dd'T'HH:mm:ss.SSSZ" i.e. 1997-07-16T19:20:30.45+01:00
|
||||
case isoDateTimeMilliSec
|
||||
|
||||
/// The dotNet formatted date "/Date(%d%d)/" i.e. "/Date(1268123281843)/"
|
||||
case dotNet
|
||||
|
||||
/// The RSS formatted date "EEE, d MMM yyyy HH:mm:ss ZZZ" i.e. "Fri, 09 Sep 2011 15:26:08 +0200"
|
||||
case rss
|
||||
|
||||
/// The Alternative RSS formatted date "d MMM yyyy HH:mm:ss ZZZ" i.e. "09 Sep 2011 15:26:08 +0200"
|
||||
case altRSS
|
||||
|
||||
/// The http header formatted date "EEE, dd MM yyyy HH:mm:ss ZZZ" i.e. "Tue, 15 Nov 1994 12:45:26 GMT"
|
||||
case httpHeader
|
||||
|
||||
/// A generic standard format date i.e. "EEE MMM dd HH:mm:ss Z yyyy"
|
||||
case standard
|
||||
|
||||
/// A custom date format string
|
||||
case custom(String)
|
||||
|
||||
/// The local formatted date and time "yyyy-MM-dd HH:mm:ss" i.e. 1997-07-16 19:20:00
|
||||
case localDateTimeSec
|
||||
|
||||
/// The local formatted date "yyyy-MM-dd" i.e. 1997-07-16
|
||||
case localDate
|
||||
|
||||
/// The local formatted time "hh:mm a" i.e. 07:20 am
|
||||
case localTimeWithNoon
|
||||
|
||||
/// The local formatted date and time "yyyyMMddHHmmss" i.e. 19970716192000
|
||||
case localPhotoSave
|
||||
|
||||
case birthDateFormatOne
|
||||
|
||||
case birthDateFormatTwo
|
||||
|
||||
///
|
||||
case messageRTetriveFormat
|
||||
|
||||
///
|
||||
case emailTimePreview
|
||||
|
||||
var stringFormat:String {
|
||||
switch self {
|
||||
//handle iso Time
|
||||
case .birthDateFormatOne: return "dd/MM/YYYY"
|
||||
case .birthDateFormatTwo: return "dd-MM-YYYY"
|
||||
case .isoYear: return "yyyy"
|
||||
case .isoYearMonth: return "yyyy-MM"
|
||||
case .isoDate: return "yyyy-MM-dd"
|
||||
case .isoDateTime: return "yyyy-MM-dd'T'HH:mmZ"
|
||||
case .isoDateTimeSec: return "yyyy-MM-dd'T'HH:mm:ssZ"
|
||||
case .isoDateTimeMilliSec: return "yyyy-MM-dd'T'HH:mm:ss.SSSZ"
|
||||
case .dotNet: return "/Date(%d%f)/"
|
||||
case .rss: return "EEE, d MMM yyyy HH:mm:ss ZZZ"
|
||||
case .altRSS: return "d MMM yyyy HH:mm:ss ZZZ"
|
||||
case .httpHeader: return "EEE, dd MM yyyy HH:mm:ss ZZZ"
|
||||
case .standard: return "EEE MMM dd HH:mm:ss Z yyyy"
|
||||
case .custom(let customFormat): return customFormat
|
||||
|
||||
//handle local Time
|
||||
case .localDateTimeSec: return "yyyy-MM-dd HH:mm:ss"
|
||||
case .localTimeWithNoon: return "hh:mm a"
|
||||
case .localDate: return "yyyy-MM-dd"
|
||||
case .localPhotoSave: return "yyyyMMddHHmmss"
|
||||
case .messageRTetriveFormat: return "yyyy-MM-dd'T'HH:mm:ssZZZZZ"
|
||||
case .emailTimePreview: return "dd MMM yyyy, h:mm a"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func toDate(_ format: DateFormatType = .isoDate) -> Date?{
|
||||
let dateFormatter = DateFormatter()
|
||||
dateFormatter.dateFormat = format.stringFormat
|
||||
let date = dateFormatter.date(from: self)
|
||||
return date
|
||||
}
|
||||
}
|
|
@ -11,13 +11,17 @@ struct HTMLFormattedText: UIViewRepresentable {
|
|||
|
||||
let text: String
|
||||
private let textView = UITextView()
|
||||
private let fontSize: Int
|
||||
private let width: Int
|
||||
|
||||
init(_ content: String) {
|
||||
init(_ content: String, withFontSize fontSize: Int = 16, andWidth width: Int? = nil) {
|
||||
self.text = content
|
||||
self.fontSize = fontSize
|
||||
self.width = width ?? Int(UIScreen.main.bounds.width) - 16
|
||||
}
|
||||
|
||||
func makeUIView(context: UIViewRepresentableContext<Self>) -> UITextView {
|
||||
textView.widthAnchor.constraint(equalToConstant:UIScreen.main.bounds.width - 16).isActive = true
|
||||
textView.widthAnchor.constraint(equalToConstant: CGFloat(self.width)).isActive = true
|
||||
textView.isSelectable = false
|
||||
textView.isUserInteractionEnabled = false
|
||||
textView.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
@ -42,12 +46,12 @@ struct HTMLFormattedText: UIViewRepresentable {
|
|||
}
|
||||
|
||||
let largeAttributes = [
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
|
||||
NSAttributedString.Key.foregroundColor: UIColor(Color("mainTextColor"))
|
||||
]
|
||||
|
||||
let linkAttributes = [
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16),
|
||||
NSAttributedString.Key.font: UIFont.systemFont(ofSize: CGFloat(self.fontSize)),
|
||||
NSAttributedString.Key.foregroundColor: UIColor(Color("AccentColor"))
|
||||
]
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
import MastodonSwift
|
||||
|
|
|
@ -0,0 +1,168 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MastodonSwift
|
||||
|
||||
public class AuthorizationService {
|
||||
public static let shared = AuthorizationService()
|
||||
|
||||
public func verifyAccount(_ result: @escaping (AccountData?) -> Void) async {
|
||||
let accountDataHandler = AccountDataHandler()
|
||||
let currentAccount = accountDataHandler.getCurrentAccountData()
|
||||
|
||||
// When we dont have even one account stored in database then we have to ask user to enter server and sign in.
|
||||
guard let accountData = currentAccount, let accessToken = accountData.accessToken else {
|
||||
result(nil)
|
||||
return
|
||||
}
|
||||
|
||||
// When we have at least one account then we have to verify access token.
|
||||
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
|
||||
|
||||
do {
|
||||
let account = try await client.verifyCredentials()
|
||||
try await self.updateAccount(accountData: accountData, account: account)
|
||||
result(accountData)
|
||||
} catch {
|
||||
do {
|
||||
try await self.refreshCredentials(accountData: accountData)
|
||||
result(accountData)
|
||||
} catch {
|
||||
// TODO: show information to the user.
|
||||
print("Cannot refresh credentials!!!")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public func signIn(serverAddress: String, _ result: @escaping (AccountData?) -> Void) async throws {
|
||||
let baseUrl = URL(string: serverAddress)!
|
||||
let client = MastodonClient(baseURL: baseUrl)
|
||||
|
||||
// Verify address.
|
||||
let instanceInformation = try await client.readInstanceInformation()
|
||||
print(instanceInformation)
|
||||
|
||||
// Create application (we will get clientId amd clientSecret).
|
||||
let oAuthApp = try await client.createApp(
|
||||
named: "Photofed",
|
||||
redirectUri: "oauth-vernissage://oauth-callback/mastodon",
|
||||
scopes: Scopes(["read", "write", "follow", "push"]),
|
||||
website: baseUrl)
|
||||
|
||||
// Authorize a user (browser, we will get clientCode).
|
||||
let oAuthSwiftCredential = try await client.authenticate(
|
||||
app: oAuthApp,
|
||||
scope: Scopes(["read", "write", "follow", "push"]))
|
||||
|
||||
// Get authenticated client.
|
||||
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
|
||||
|
||||
// Get account information from server.
|
||||
let account = try await authenticatedClient.verifyCredentials()
|
||||
|
||||
// Create account object in database.
|
||||
let accountDataHandler = AccountDataHandler()
|
||||
let accountData = accountDataHandler.createAccountDataEntity()
|
||||
|
||||
accountData.id = account.id
|
||||
accountData.username = account.username
|
||||
accountData.acct = account.acct
|
||||
accountData.displayName = account.displayName
|
||||
accountData.note = account.note
|
||||
accountData.url = account.url
|
||||
accountData.avatar = account.avatar
|
||||
accountData.header = account.header
|
||||
accountData.locked = account.locked
|
||||
accountData.createdAt = account.createdAt
|
||||
accountData.followersCount = Int32(account.followersCount)
|
||||
accountData.followingCount = Int32(account.followingCount)
|
||||
accountData.statusesCount = Int32(account.statusesCount)
|
||||
|
||||
accountData.serverUrl = baseUrl
|
||||
accountData.clientId = oAuthApp.clientId
|
||||
accountData.clientSecret = oAuthApp.clientSecret
|
||||
accountData.clientVapidKey = oAuthApp.vapidKey ?? ""
|
||||
accountData.accessToken = oAuthSwiftCredential.oauthToken
|
||||
|
||||
// Download avatar image.
|
||||
if let avatarUrl = account.avatar {
|
||||
do {
|
||||
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
|
||||
accountData.avatarData = avatarData
|
||||
}
|
||||
catch {
|
||||
print("Avatar has not been downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
// Set newly created account as current.
|
||||
let applicationSettingsHandler = ApplicationSettingsHandler()
|
||||
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
|
||||
defaultSettings.currentAccount = accountData.id
|
||||
|
||||
// Save account data in database and in application state.
|
||||
CoreDataHandler.shared.save()
|
||||
|
||||
// Return account data.
|
||||
result(accountData)
|
||||
}
|
||||
|
||||
private func refreshCredentials(accountData: AccountData) async throws {
|
||||
let client = MastodonClient(baseURL: accountData.serverUrl)
|
||||
|
||||
// Create application (we will get clientId amd clientSecret).
|
||||
let oAuthApp = App(clientId: accountData.clientId, clientSecret: accountData.clientSecret)
|
||||
|
||||
// Authorize a user (browser, we will get clientCode).
|
||||
let oAuthSwiftCredential = try await client.authenticate(app: oAuthApp, scope: Scopes(["read", "write", "follow", "push"]))
|
||||
|
||||
// Get authenticated client.
|
||||
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
|
||||
|
||||
// Get account information from server.
|
||||
let account = try await authenticatedClient.verifyCredentials()
|
||||
try await self.updateAccount(accountData: accountData, account: account, accessToken: oAuthSwiftCredential.oauthToken)
|
||||
}
|
||||
|
||||
private func updateAccount(accountData: AccountData, account: Account, accessToken: String? = nil) async throws {
|
||||
accountData.username = account.username
|
||||
accountData.acct = account.acct
|
||||
accountData.displayName = account.displayName
|
||||
accountData.note = account.note
|
||||
accountData.url = account.url
|
||||
accountData.avatar = account.avatar
|
||||
accountData.header = account.header
|
||||
accountData.locked = account.locked
|
||||
accountData.createdAt = account.createdAt
|
||||
accountData.followersCount = Int32(account.followersCount)
|
||||
accountData.followingCount = Int32(account.followingCount)
|
||||
accountData.statusesCount = Int32(account.statusesCount)
|
||||
|
||||
if accessToken != nil {
|
||||
accountData.accessToken = accessToken
|
||||
}
|
||||
|
||||
// Download avatar image.
|
||||
if let avatarUrl = account.avatar {
|
||||
do {
|
||||
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
|
||||
accountData.avatarData = avatarData
|
||||
}
|
||||
catch {
|
||||
print("Avatar has not been downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
// We have to be sure that account id is saved as default account.
|
||||
let applicationSettingsHandler = ApplicationSettingsHandler()
|
||||
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
|
||||
defaultSettings.currentAccount = accountData.id
|
||||
|
||||
// Save account data in database and in application state.
|
||||
CoreDataHandler.shared.save()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,136 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CoreData
|
||||
import MastodonSwift
|
||||
|
||||
public class TimelineService {
|
||||
public static let shared = TimelineService()
|
||||
|
||||
public func onBottomOfList(for accountData: AccountData) async throws {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let statusDataHandler = StatusDataHandler()
|
||||
let oldestStatus = statusDataHandler.getMinimumtatus(viewContext: backgroundContext)
|
||||
|
||||
guard let oldestStatus = oldestStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
try await self.loadData(for: accountData, on: backgroundContext, maxId: oldestStatus.id)
|
||||
}
|
||||
|
||||
public func onTopOfList(for accountData: AccountData) async throws {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let statusDataHandler = StatusDataHandler()
|
||||
let newestStatus = statusDataHandler.getMaximumStatus(viewContext: backgroundContext)
|
||||
|
||||
guard let newestStatus = newestStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
try await self.loadData(for: accountData, on: backgroundContext, minId: newestStatus.id)
|
||||
}
|
||||
|
||||
public func getComments(for statusId: String, and accountData: AccountData) async throws -> Context {
|
||||
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accountData.accessToken ?? "")
|
||||
return try await client.getContext(for: statusId)
|
||||
}
|
||||
|
||||
private func loadData(for accountData: AccountData, on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws {
|
||||
guard let accessToken = accountData.accessToken else {
|
||||
return
|
||||
}
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let attachmentDataHandler = AttachmentDataHandler()
|
||||
let statusDataHandler = StatusDataHandler()
|
||||
|
||||
// Retrieve statuses from API.
|
||||
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
|
||||
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40)
|
||||
|
||||
// Download status images and save it into database.
|
||||
for status in statuses {
|
||||
|
||||
// Save status data in database.
|
||||
let statusDataEntity = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext)
|
||||
statusDataEntity.accountAvatar = status.account?.avatar
|
||||
statusDataEntity.accountDisplayName = status.account?.displayName
|
||||
statusDataEntity.accountId = status.account!.id
|
||||
statusDataEntity.accountUsername = status.account!.username
|
||||
statusDataEntity.applicationName = status.application?.name
|
||||
statusDataEntity.applicationWebsite = status.application?.website
|
||||
statusDataEntity.bookmarked = status.bookmarked
|
||||
statusDataEntity.content = status.content
|
||||
statusDataEntity.createdAt = status.createdAt
|
||||
statusDataEntity.favourited = status.favourited
|
||||
statusDataEntity.favouritesCount = Int32(status.favouritesCount)
|
||||
statusDataEntity.id = status.id
|
||||
statusDataEntity.inReplyToAccount = status.inReplyToAccount
|
||||
statusDataEntity.inReplyToId = status.inReplyToId
|
||||
statusDataEntity.muted = status.muted
|
||||
statusDataEntity.pinned = status.pinned
|
||||
statusDataEntity.reblogged = status.reblogged
|
||||
statusDataEntity.reblogsCount = Int32(status.reblogsCount)
|
||||
statusDataEntity.sensitive = status.sensitive
|
||||
statusDataEntity.spoilerText = status.spoilerText
|
||||
statusDataEntity.uri = status.uri
|
||||
statusDataEntity.url = status.url
|
||||
statusDataEntity.visibility = status.visibility.rawValue
|
||||
|
||||
for attachment in status.mediaAttachments {
|
||||
let imageData = try await self.fetchImage(attachment: attachment)
|
||||
|
||||
guard let imageData = imageData else {
|
||||
continue
|
||||
}
|
||||
|
||||
/*
|
||||
var exif = image.getExifData()
|
||||
if let dict = exif as? [String: AnyObject] {
|
||||
dict.keys.map { key in
|
||||
print(key)
|
||||
print(dict[key])
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Save attachment in database.
|
||||
let attachmentData = attachmentDataHandler.createAttachmnentDataEntity(viewContext: backgroundContext)
|
||||
attachmentData.id = attachment.id
|
||||
attachmentData.url = attachment.url
|
||||
attachmentData.blurhash = attachment.blurhash
|
||||
attachmentData.previewUrl = attachment.previewUrl
|
||||
attachmentData.remoteUrl = attachment.remoteUrl
|
||||
attachmentData.text = attachment.description
|
||||
attachmentData.type = attachment.type.rawValue
|
||||
|
||||
attachmentData.statusId = statusDataEntity.id
|
||||
attachmentData.data = imageData
|
||||
|
||||
attachmentData.statusRelation = statusDataEntity
|
||||
statusDataEntity.addToAttachmentRelation(attachmentData)
|
||||
}
|
||||
}
|
||||
|
||||
try backgroundContext.save()
|
||||
}
|
||||
|
||||
private func fetchImage(attachment: Attachment) async throws -> Data? {
|
||||
guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
|
@ -5,10 +5,9 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import MastodonSwift
|
||||
|
||||
@main
|
||||
struct VernissageApp: SwiftUI.App {
|
||||
struct VernissageApp: App {
|
||||
@UIApplicationDelegateAdaptor(AppDelegate.self) var appDelegate
|
||||
|
||||
let coreDataHandler = CoreDataHandler.shared
|
||||
|
@ -35,97 +34,19 @@ struct VernissageApp: SwiftUI.App {
|
|||
}
|
||||
}
|
||||
.task {
|
||||
let accountDataHandler = AccountDataHandler()
|
||||
let currentAccount = accountDataHandler.getCurrentAccountData()
|
||||
|
||||
// When we dont have even one account stored in database then we have to ask user to enter server and sign in.
|
||||
guard let accountData = currentAccount, let accessToken = accountData.accessToken else {
|
||||
self.applicationViewMode = .signIn
|
||||
return
|
||||
}
|
||||
|
||||
// When we have at least one account then we have to verify access token.
|
||||
let client = MastodonClient(baseURL: accountData.serverUrl).getAuthenticated(token: accessToken)
|
||||
|
||||
do {
|
||||
let account = try await client.verifyCredentials()
|
||||
try await self.updateAccount(accountData: accountData, account: account)
|
||||
|
||||
self.applicationViewMode = .mainView
|
||||
self.applicationState.accountData = accountData
|
||||
} catch {
|
||||
do {
|
||||
try await self.refreshCredentials(accountData: accountData)
|
||||
|
||||
self.applicationViewMode = .mainView
|
||||
self.applicationState.accountData = accountData
|
||||
} catch {
|
||||
// TODO: show information to the user.
|
||||
print("Cannot refresh credentials!!!")
|
||||
await AuthorizationService.shared.verifyAccount({ accountData in
|
||||
guard let accountData = accountData else {
|
||||
self.applicationViewMode = .signIn
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.applicationState.accountData = accountData
|
||||
self.applicationViewMode = .mainView
|
||||
})
|
||||
}
|
||||
.navigationViewStyle(.stack)
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshCredentials(accountData: AccountData) async throws {
|
||||
let client = MastodonClient(baseURL: accountData.serverUrl)
|
||||
|
||||
// Create application (we will get clientId amd clientSecret).
|
||||
let oAuthApp = App(clientId: accountData.clientId, clientSecret: accountData.clientSecret)
|
||||
|
||||
// Authorize a user (browser, we will get clientCode).
|
||||
let oAuthSwiftCredential = try await client.authenticate(app: oAuthApp, scope: Scopes(["read", "write", "follow", "push"]))
|
||||
|
||||
// Get authenticated client.
|
||||
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
|
||||
|
||||
// Get account information from server.
|
||||
let account = try await authenticatedClient.verifyCredentials()
|
||||
try await self.updateAccount(accountData: accountData, account: account, accessToken: oAuthSwiftCredential.oauthToken)
|
||||
|
||||
self.applicationState.accountData = accountData
|
||||
self.applicationViewMode = .mainView
|
||||
}
|
||||
|
||||
private func updateAccount(accountData: AccountData, account: Account, accessToken: String? = nil) async throws {
|
||||
accountData.username = account.username
|
||||
accountData.acct = account.acct
|
||||
accountData.displayName = account.displayName
|
||||
accountData.note = account.note
|
||||
accountData.url = account.url
|
||||
accountData.avatar = account.avatar
|
||||
accountData.header = account.header
|
||||
accountData.locked = account.locked
|
||||
accountData.createdAt = account.createdAt
|
||||
accountData.followersCount = Int32(account.followersCount)
|
||||
accountData.followingCount = Int32(account.followingCount)
|
||||
accountData.statusesCount = Int32(account.statusesCount)
|
||||
|
||||
if accessToken != nil {
|
||||
accountData.accessToken = accessToken
|
||||
}
|
||||
|
||||
// Download avatar image.
|
||||
if let avatarUrl = account.avatar {
|
||||
do {
|
||||
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
|
||||
accountData.avatarData = avatarData
|
||||
}
|
||||
catch {
|
||||
print("Avatar has not been downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
// We have to be sure that account id is saved as default account.
|
||||
let applicationSettingsHandler = ApplicationSettingsHandler()
|
||||
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
|
||||
defaultSettings.currentAccount = accountData.id
|
||||
|
||||
// Save account data in database and in application state.
|
||||
try self.coreDataHandler.container.viewContext.save()
|
||||
}
|
||||
}
|
||||
|
||||
class AppDelegate: NSObject, UIApplicationDelegate {
|
||||
|
|
|
@ -9,120 +9,50 @@ import MastodonSwift
|
|||
import AVFoundation
|
||||
|
||||
struct DetailsView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State public var statusData: StatusData
|
||||
@State private var height: Double = 0.0
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
VStack (alignment: .leading) {
|
||||
TabView {
|
||||
ForEach(statusData.attachments(), id: \.self) { attachment in
|
||||
if let image = UIImage(data: attachment.data) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: CGFloat(self.height))
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
|
||||
ImagesCarousel(attachments: statusData.attachments())
|
||||
|
||||
VStack(alignment: .leading) {
|
||||
HStack (alignment: .center) {
|
||||
AsyncImage(url: statusData.accountAvatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
.foregroundColor(Color("mainTextColor"))
|
||||
}
|
||||
.frame(width: 48.0, height: 48.0)
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
Text(statusData.accountDisplayName ?? statusData.accountUsername)
|
||||
.foregroundColor(Color("displayNameColor"))
|
||||
Text("@\(statusData.accountUsername)")
|
||||
.foregroundColor(Color("lightGrayColor"))
|
||||
.font(.footnote)
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
|
||||
UsernameRow(statusData: statusData)
|
||||
HTMLFormattedText(statusData.content)
|
||||
.padding(.leading, -4)
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
LabelIconView(iconName: "camera", value: "SONY ILCE-7M3")
|
||||
LabelIconView(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
||||
LabelIconView(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
||||
LabelIconView(iconName: "calendar", value: "2 Oct 2022")
|
||||
LabelIcon(iconName: "camera", value: "SONY ILCE-7M3")
|
||||
LabelIcon(iconName: "camera.aperture", value: "Viltrox 24mm F1.8 E")
|
||||
LabelIcon(iconName: "timelapse", value: "24.0 mm, f/1.8, 1/640s, ISO 100")
|
||||
LabelIcon(iconName: "calendar", value: "2 Oct 2022")
|
||||
}
|
||||
.foregroundColor(Color("lightGrayColor"))
|
||||
|
||||
HStack (alignment: .top) {
|
||||
TagView {
|
||||
// Favorite
|
||||
} content: {
|
||||
HStack {
|
||||
Image(systemName: statusData.favourited ? "heart.fill" : "heart")
|
||||
Text("\(statusData.favouritesCount) likes")
|
||||
}
|
||||
}
|
||||
|
||||
TagView {
|
||||
// Reboost
|
||||
} content: {
|
||||
HStack {
|
||||
Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward")
|
||||
Text("\(statusData.reblogsCount) boosts")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
TagView {
|
||||
// Bookmark
|
||||
} content: {
|
||||
Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark")
|
||||
}
|
||||
HStack {
|
||||
Text("Uploaded")
|
||||
Text(statusData.createdAt.toDate(.isoDateTimeMilliSec) ?? Date(), style: .relative)
|
||||
.padding(.horizontal, -4)
|
||||
Text("ago")
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color("mainTextColor"))
|
||||
.foregroundColor(Color("lightGrayColor"))
|
||||
.font(.footnote)
|
||||
|
||||
InteractionRow(statusData: statusData)
|
||||
}
|
||||
.padding(8)
|
||||
|
||||
CommentsSection(statusId: statusData.id)
|
||||
}
|
||||
}
|
||||
.navigationBarTitle("Details")
|
||||
.onAppear {
|
||||
self.calculateImageHeight()
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateImageHeight() {
|
||||
var imageHeight = 0.0
|
||||
var imageWidth = 0.0
|
||||
|
||||
for item in statusData.attachments() {
|
||||
if let image = UIImage(data: item.data) {
|
||||
if image.size.height > imageHeight {
|
||||
imageHeight = image.size.height
|
||||
imageWidth = image.size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let divider = imageWidth / UIScreen.main.bounds.size.width
|
||||
self.height = imageHeight / divider
|
||||
}
|
||||
}
|
||||
|
||||
struct DetailsView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
Text("")
|
||||
// DetailsView(current: ImageStatus(id: "123", image: UIImage(), status: Status(from: <#T##Decoder#>)))
|
||||
DetailsView(statusData: StatusData())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -5,9 +5,6 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import MastodonSwift
|
||||
import UIKit
|
||||
import CoreData
|
||||
|
||||
struct HomeFeedView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
@ -25,33 +22,10 @@ struct HomeFeedView: View {
|
|||
ScrollView {
|
||||
LazyVGrid(columns: gridColumns) {
|
||||
ForEach(dbStatuses, id: \.self) { item in
|
||||
NavigationLink(destination: DetailsView(statusData: item)) {
|
||||
if let attachmenData = item.attachmentRelation?.first,
|
||||
let uiImage = UIImage(data: attachmenData.data) {
|
||||
|
||||
ZStack {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
||||
if let count = item.attachmentRelation?.count, count > 1 {
|
||||
VStack(alignment:.trailing) {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
Text("1 / \(count)")
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.black)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
}
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Error")
|
||||
}
|
||||
NavigationLink(destination:
|
||||
DetailsView(statusData: item)
|
||||
.environmentObject(applicationState)) {
|
||||
ImageRow(attachments: item.attachments())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -60,7 +34,9 @@ struct HomeFeedView: View {
|
|||
.onAppear {
|
||||
Task {
|
||||
do {
|
||||
try await onBottomOfList()
|
||||
if let accountData = self.applicationState.accountData {
|
||||
try await TimelineService.shared.onBottomOfList(for: accountData)
|
||||
}
|
||||
} catch {
|
||||
print("Error", error)
|
||||
}
|
||||
|
@ -76,7 +52,9 @@ struct HomeFeedView: View {
|
|||
}
|
||||
.refreshable {
|
||||
do {
|
||||
try await onTopOfList()
|
||||
if let accountData = self.applicationState.accountData {
|
||||
try await TimelineService.shared.onTopOfList(for: accountData)
|
||||
}
|
||||
} catch {
|
||||
print("Error", error)
|
||||
}
|
||||
|
@ -85,7 +63,9 @@ struct HomeFeedView: View {
|
|||
do {
|
||||
if self.dbStatuses.isEmpty {
|
||||
self.showLoading = true
|
||||
try await onTopOfList()
|
||||
if let accountData = self.applicationState.accountData {
|
||||
try await TimelineService.shared.onTopOfList(for: accountData)
|
||||
}
|
||||
self.showLoading = false
|
||||
}
|
||||
} catch {
|
||||
|
@ -94,124 +74,6 @@ struct HomeFeedView: View {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func onBottomOfList() async throws {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let statusDataHandler = StatusDataHandler()
|
||||
let oldestStatus = statusDataHandler.getMinimumtatus(viewContext: backgroundContext)
|
||||
|
||||
guard let oldestStatus = oldestStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
try await self.loadData(on: backgroundContext, maxId: oldestStatus.id)
|
||||
}
|
||||
|
||||
private func onTopOfList() async throws {
|
||||
// Load data from API and operate on CoreData on background context.
|
||||
let backgroundContext = CoreDataHandler.shared.newBackgroundContext()
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let statusDataHandler = StatusDataHandler()
|
||||
let newestStatus = statusDataHandler.getMaximumStatus(viewContext: backgroundContext)
|
||||
|
||||
guard let newestStatus = newestStatus else {
|
||||
return
|
||||
}
|
||||
|
||||
try await self.loadData(on: backgroundContext, minId: newestStatus.id)
|
||||
}
|
||||
|
||||
private func loadData(on backgroundContext: NSManagedObjectContext, minId: String? = nil, maxId: String? = nil) async throws {
|
||||
guard let accessData = self.applicationState.accountData, let accessToken = accessData.accessToken else {
|
||||
return
|
||||
}
|
||||
|
||||
// Get maximimum downloaded stauts id.
|
||||
let attachmentDataHandler = AttachmentDataHandler()
|
||||
let statusDataHandler = StatusDataHandler()
|
||||
|
||||
// Retrieve statuses from API.
|
||||
let client = MastodonClient(baseURL: accessData.serverUrl).getAuthenticated(token: accessToken)
|
||||
let statuses = try await client.getHomeTimeline(maxId: maxId, minId: minId, limit: 40)
|
||||
|
||||
// Download status images and save it into database.
|
||||
for status in statuses {
|
||||
|
||||
// Save status data in database.
|
||||
let statusDataEntity = statusDataHandler.createStatusDataEntity(viewContext: backgroundContext)
|
||||
statusDataEntity.accountAvatar = status.account?.avatar
|
||||
statusDataEntity.accountDisplayName = status.account?.displayName
|
||||
statusDataEntity.accountId = status.account!.id
|
||||
statusDataEntity.accountUsername = status.account!.username
|
||||
statusDataEntity.applicationName = status.application?.name
|
||||
statusDataEntity.applicationWebsite = status.application?.website
|
||||
statusDataEntity.bookmarked = status.bookmarked
|
||||
statusDataEntity.content = status.content
|
||||
statusDataEntity.createdAt = status.createdAt
|
||||
statusDataEntity.favourited = status.favourited
|
||||
statusDataEntity.favouritesCount = Int32(status.favouritesCount)
|
||||
statusDataEntity.id = status.id
|
||||
statusDataEntity.inReplyToAccount = status.inReplyToAccount
|
||||
statusDataEntity.inReplyToId = status.inReplyToId
|
||||
statusDataEntity.muted = status.muted
|
||||
statusDataEntity.pinned = status.pinned
|
||||
statusDataEntity.reblogged = status.reblogged
|
||||
statusDataEntity.reblogsCount = Int32(status.reblogsCount)
|
||||
statusDataEntity.sensitive = status.sensitive
|
||||
statusDataEntity.spoilerText = status.spoilerText
|
||||
statusDataEntity.uri = status.uri
|
||||
statusDataEntity.url = status.url
|
||||
statusDataEntity.visibility = status.visibility.rawValue
|
||||
|
||||
for attachment in status.mediaAttachments {
|
||||
let imageData = try await self.fetchImage(attachment: attachment)
|
||||
|
||||
guard let imageData = imageData else {
|
||||
continue
|
||||
}
|
||||
|
||||
/*
|
||||
var exif = image.getExifData()
|
||||
if let dict = exif as? [String: AnyObject] {
|
||||
dict.keys.map { key in
|
||||
print(key)
|
||||
print(dict[key])
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
// Save attachment in database.
|
||||
let attachmentData = attachmentDataHandler.createAttachmnentDataEntity(viewContext: backgroundContext)
|
||||
attachmentData.id = attachment.id
|
||||
attachmentData.url = attachment.url
|
||||
attachmentData.blurhash = attachment.blurhash
|
||||
attachmentData.previewUrl = attachment.previewUrl
|
||||
attachmentData.remoteUrl = attachment.remoteUrl
|
||||
attachmentData.text = attachment.description
|
||||
attachmentData.type = attachment.type.rawValue
|
||||
|
||||
attachmentData.statusId = statusDataEntity.id
|
||||
attachmentData.data = imageData
|
||||
|
||||
attachmentData.statusRelation = statusDataEntity
|
||||
statusDataEntity.addToAttachmentRelation(attachmentData)
|
||||
}
|
||||
}
|
||||
|
||||
try backgroundContext.save()
|
||||
}
|
||||
|
||||
public func fetchImage(attachment: Attachment) async throws -> Data? {
|
||||
guard let data = try await RemoteFileService.shared.fetchData(url: attachment.url) else {
|
||||
return nil
|
||||
}
|
||||
|
||||
return data
|
||||
}
|
||||
}
|
||||
|
||||
struct HomeFeedView_Previews: PreviewProvider {
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -3,7 +3,6 @@
|
|||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
//
|
||||
|
||||
import SwiftUI
|
||||
import MastodonSwift
|
||||
|
||||
struct SignInView: View {
|
||||
@Environment(\.managedObjectContext) private var viewContext
|
||||
|
@ -29,7 +28,10 @@ struct SignInView: View {
|
|||
|
||||
Button("Go") {
|
||||
Task {
|
||||
try await self.signIn()
|
||||
try await AuthorizationService.shared.signIn(serverAddress: serverAddress, { accountData in
|
||||
self.applicationState.accountData = accountData
|
||||
onSignInStateChenge(.mainView)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -38,79 +40,6 @@ struct SignInView: View {
|
|||
.navigationBarTitle("Sign in to Pixelfed")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
}
|
||||
|
||||
private func signIn() async throws {
|
||||
let baseUrl = URL(string: serverAddress)!
|
||||
let client = MastodonClient(baseURL: baseUrl)
|
||||
|
||||
// Verify address.
|
||||
let instanceInformation = try await client.readInstanceInformation()
|
||||
print(instanceInformation)
|
||||
|
||||
// Create application (we will get clientId amd clientSecret).
|
||||
let oAuthApp = try await client.createApp(
|
||||
named: "Photofed",
|
||||
redirectUri: "oauth-vernissage://oauth-callback/mastodon",
|
||||
scopes: Scopes(["read", "write", "follow", "push"]),
|
||||
website: baseUrl)
|
||||
|
||||
// Authorize a user (browser, we will get clientCode).
|
||||
let oAuthSwiftCredential = try await client.authenticate(
|
||||
app: oAuthApp,
|
||||
scope: Scopes(["read", "write", "follow", "push"]))
|
||||
|
||||
// Get authenticated client.
|
||||
let authenticatedClient = client.getAuthenticated(token: oAuthSwiftCredential.oauthToken)
|
||||
|
||||
// Get account information from server.
|
||||
let account = try await authenticatedClient.verifyCredentials()
|
||||
|
||||
// Create account object in database.
|
||||
let accountDataHandler = AccountDataHandler()
|
||||
let accountData = accountDataHandler.createAccountDataEntity()
|
||||
|
||||
accountData.id = account.id
|
||||
accountData.username = account.username
|
||||
accountData.acct = account.acct
|
||||
accountData.displayName = account.displayName
|
||||
accountData.note = account.note
|
||||
accountData.url = account.url
|
||||
accountData.avatar = account.avatar
|
||||
accountData.header = account.header
|
||||
accountData.locked = account.locked
|
||||
accountData.createdAt = account.createdAt
|
||||
accountData.followersCount = Int32(account.followersCount)
|
||||
accountData.followingCount = Int32(account.followingCount)
|
||||
accountData.statusesCount = Int32(account.statusesCount)
|
||||
|
||||
accountData.serverUrl = baseUrl
|
||||
accountData.clientId = oAuthApp.clientId
|
||||
accountData.clientSecret = oAuthApp.clientSecret
|
||||
accountData.clientVapidKey = oAuthApp.vapidKey ?? ""
|
||||
accountData.accessToken = oAuthSwiftCredential.oauthToken
|
||||
|
||||
// Download avatar image.
|
||||
if let avatarUrl = account.avatar {
|
||||
do {
|
||||
let avatarData = try await RemoteFileService.shared.fetchData(url: avatarUrl)
|
||||
accountData.avatarData = avatarData
|
||||
}
|
||||
catch {
|
||||
print("Avatar has not been downloaded")
|
||||
}
|
||||
}
|
||||
|
||||
// Set newly created account as current.
|
||||
let applicationSettingsHandler = ApplicationSettingsHandler()
|
||||
let defaultSettings = applicationSettingsHandler.getDefaultSettings()
|
||||
defaultSettings.currentAccount = accountData.id
|
||||
|
||||
// Save account data in database and in application state.
|
||||
try self.viewContext.save()
|
||||
|
||||
self.applicationState.accountData = accountData
|
||||
self.onSignInStateChenge(.mainView)
|
||||
}
|
||||
}
|
||||
|
||||
struct SignInView_Previews: PreviewProvider {
|
||||
|
|
|
@ -0,0 +1,33 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct BottomRight<Content: View>: View {
|
||||
let content: Content
|
||||
|
||||
init(@ViewBuilder content: () -> Content) {
|
||||
self.content = content()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment:.trailing) {
|
||||
Spacer()
|
||||
HStack {
|
||||
Spacer()
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct BottomRight_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
BottomRight {
|
||||
Text("1/2")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import MastodonSwift
|
||||
|
||||
struct CommentsSection: View {
|
||||
@EnvironmentObject var applicationState: ApplicationState
|
||||
|
||||
@State public var statusId: String
|
||||
@State public var withDivider = true
|
||||
@State private var context: Context?
|
||||
|
||||
private let contentWidth = Int(UIScreen.main.bounds.width) - 50
|
||||
|
||||
var body: some View {
|
||||
VStack {
|
||||
if let context = context {
|
||||
ForEach(context.descendants, id: \.id) { status in
|
||||
HStack (alignment: .top) {
|
||||
AsyncImage(url: status.account?.avatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
.foregroundColor(Color("mainTextColor"))
|
||||
}
|
||||
.frame(width: 32.0, height: 32.0)
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
HStack (alignment: .top) {
|
||||
Text(status.account?.displayName ?? status.account?.username ?? "")
|
||||
.foregroundColor(Color("displayNameColor"))
|
||||
.font(.footnote)
|
||||
.fontWeight(.bold)
|
||||
Text("@\(status.account?.username ?? "")")
|
||||
.foregroundColor(Color("lightGrayColor"))
|
||||
.font(.footnote)
|
||||
}
|
||||
.padding(.bottom, -10)
|
||||
|
||||
HTMLFormattedText(status.content, withFontSize: 14, andWidth: contentWidth)
|
||||
.padding(.leading, -4)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 8)
|
||||
|
||||
CommentsSection(statusId: status.id, withDivider: false)
|
||||
|
||||
if withDivider {
|
||||
Rectangle()
|
||||
.size(width: UIScreen.main.bounds.width, height: 4)
|
||||
.fill(Color("mainTextColor"))
|
||||
.opacity(0.05)
|
||||
}
|
||||
}
|
||||
}
|
||||
}.task {
|
||||
do {
|
||||
if let accountData = applicationState.accountData {
|
||||
self.context = try await TimelineService.shared.getComments(for: statusId, and: accountData)
|
||||
}
|
||||
} catch {
|
||||
print("Error \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct CommentsSection_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
CommentsSection(statusId: "", withDivider: true)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImageRow: View {
|
||||
@State public var attachments: [AttachmentData]
|
||||
|
||||
var body: some View {
|
||||
if let attachmenData = attachments.first,
|
||||
let uiImage = UIImage(data: attachmenData.data) {
|
||||
|
||||
ZStack {
|
||||
Image(uiImage: uiImage)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
|
||||
if let count = attachments.count, count > 1 {
|
||||
BottomRight {
|
||||
Text("1 / \(count)")
|
||||
.padding(.horizontal, 6)
|
||||
.padding(.vertical, 3)
|
||||
.font(.caption2)
|
||||
.foregroundColor(.black)
|
||||
.background(.ultraThinMaterial, in: Capsule())
|
||||
}.padding()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Text("Error")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ImageRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImageRow(attachments: [])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ImagesCarousel: View {
|
||||
@State public var attachments: [AttachmentData]
|
||||
@State private var height: Double = 0.0
|
||||
|
||||
var body: some View {
|
||||
TabView {
|
||||
ForEach(attachments, id: \.self) { attachment in
|
||||
if let image = UIImage(data: attachment.data) {
|
||||
Image(uiImage: image)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(height: CGFloat(self.height))
|
||||
.tabViewStyle(PageTabViewStyle())
|
||||
.onAppear {
|
||||
self.calculateImageHeight()
|
||||
}
|
||||
}
|
||||
|
||||
private func calculateImageHeight() {
|
||||
var imageHeight = 0.0
|
||||
var imageWidth = 0.0
|
||||
|
||||
for item in attachments {
|
||||
if let image = UIImage(data: item.data) {
|
||||
if image.size.height > imageHeight {
|
||||
imageHeight = image.size.height
|
||||
imageWidth = image.size.width
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let divider = imageWidth / UIScreen.main.bounds.size.width
|
||||
self.height = imageHeight / divider
|
||||
}
|
||||
}
|
||||
|
||||
struct ImagesCarousel_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
ImagesCarousel(attachments: [])
|
||||
}
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct InteractionRow: View {
|
||||
@State public var statusData: StatusData
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .top) {
|
||||
Tag {
|
||||
// Favorite
|
||||
} content: {
|
||||
HStack {
|
||||
Image(systemName: statusData.favourited ? "heart.fill" : "heart")
|
||||
Text("\(statusData.favouritesCount) likes")
|
||||
}
|
||||
}
|
||||
|
||||
Tag {
|
||||
// Reboost
|
||||
} content: {
|
||||
HStack {
|
||||
Image(systemName: statusData.reblogged ? "arrowshape.turn.up.forward.fill" : "arrowshape.turn.up.forward")
|
||||
Text("\(statusData.reblogsCount) boosts")
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
Tag {
|
||||
// Bookmark
|
||||
} content: {
|
||||
Image(systemName: statusData.bookmarked ? "bookmark.fill" : "bookmark")
|
||||
}
|
||||
}
|
||||
.font(.subheadline)
|
||||
.foregroundColor(Color("mainTextColor"))
|
||||
}
|
||||
}
|
||||
|
||||
struct InteractionRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InteractionRow(statusData: StatusData())
|
||||
}
|
||||
}
|
|
@ -3,11 +3,10 @@
|
|||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct LabelIconView: View {
|
||||
struct LabelIcon: View {
|
||||
let iconName: String
|
||||
let value: String
|
||||
|
||||
|
@ -24,6 +23,6 @@ struct LabelIconView: View {
|
|||
|
||||
struct LabelIconView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
LabelIconView(iconName: "camera", value: "Sony A7")
|
||||
LabelIcon(iconName: "camera", value: "Sony A7")
|
||||
}
|
||||
}
|
|
@ -3,11 +3,10 @@
|
|||
// Copyright © 2022 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct TagView<Content: View>: View {
|
||||
struct Tag<Content: View>: View {
|
||||
let content: Content
|
||||
let action: () -> Void
|
||||
|
||||
|
@ -31,7 +30,7 @@ struct TagView<Content: View>: View {
|
|||
|
||||
struct TagView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
TagView {
|
||||
Tag {
|
||||
|
||||
} content: {
|
||||
HStack {
|
|
@ -0,0 +1,42 @@
|
|||
//
|
||||
// https://mczachurski.dev
|
||||
// Copyright © 2023 Marcin Czachurski and the repository contributors.
|
||||
// Licensed under the MIT License.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct UsernameRow: View {
|
||||
@State public var statusData: StatusData
|
||||
|
||||
var body: some View {
|
||||
HStack (alignment: .center) {
|
||||
AsyncImage(url: statusData.accountAvatar) { image in
|
||||
image
|
||||
.resizable()
|
||||
.clipShape(Circle())
|
||||
.aspectRatio(contentMode: .fit)
|
||||
} placeholder: {
|
||||
Image(systemName: "person.circle")
|
||||
.resizable()
|
||||
.foregroundColor(Color("mainTextColor"))
|
||||
}
|
||||
.frame(width: 48.0, height: 48.0)
|
||||
|
||||
VStack (alignment: .leading) {
|
||||
Text(statusData.accountDisplayName ?? statusData.accountUsername)
|
||||
.foregroundColor(Color("displayNameColor"))
|
||||
Text("@\(statusData.accountUsername)")
|
||||
.foregroundColor(Color("lightGrayColor"))
|
||||
.font(.footnote)
|
||||
}
|
||||
.padding(.leading, 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct UsernameRow_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
UsernameRow(statusData: StatusData())
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue