diff --git a/Vernissage.xcodeproj/project.pbxproj b/Vernissage.xcodeproj/project.pbxproj index 0bb9ea6..39cebe8 100644 --- a/Vernissage.xcodeproj/project.pbxproj +++ b/Vernissage.xcodeproj/project.pbxproj @@ -7,22 +7,33 @@ objects = { /* Begin PBXBuildFile section */ + F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F8F295C636C009C8EE6 /* UIImage.swift */; }; + F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = F8341F91295C63BB009C8EE6 /* ImageStatus.swift */; }; F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; }; - F88C246E295C37B80006098B /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* ContentView.swift */; }; + F88C246E295C37B80006098B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* MainView.swift */; }; F88C2470295C37BB0006098B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C246F295C37BB0006098B /* Assets.xcassets */; }; F88C2473295C37BB0006098B /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C2472295C37BB0006098B /* Preview Assets.xcassets */; }; F88C2475295C37BB0006098B /* Persistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2474295C37BB0006098B /* Persistence.swift */; }; F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */ = {isa = PBXBuildFile; fileRef = F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */; }; + F88C2480295C38400006098B /* MastodonSwift in Frameworks */ = {isa = PBXBuildFile; productRef = F88C247F295C38400006098B /* MastodonSwift */; }; + F88C2482295C3A4F0006098B /* ImageDetailsModalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2481295C3A4F0006098B /* ImageDetailsModalView.swift */; }; + F88C2484295C3A870006098B /* DismissButtonView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2483295C3A870006098B /* DismissButtonView.swift */; }; + F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C2485295C48030006098B /* HTMLFotmattedText.swift */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + F8341F8F295C636C009C8EE6 /* UIImage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UIImage.swift; sourceTree = ""; }; + F8341F91295C63BB009C8EE6 /* ImageStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageStatus.swift; sourceTree = ""; }; F88C2468295C37B80006098B /* Vernissage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vernissage.app; sourceTree = BUILT_PRODUCTS_DIR; }; F88C246B295C37B80006098B /* VernissageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VernissageApp.swift; sourceTree = ""; }; - F88C246D295C37B80006098B /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; + F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = ""; }; F88C246F295C37BB0006098B /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; F88C2472295C37BB0006098B /* Preview Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = "Preview Assets.xcassets"; sourceTree = ""; }; F88C2474295C37BB0006098B /* Persistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Persistence.swift; sourceTree = ""; }; F88C2477295C37BB0006098B /* Vernissage.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = Vernissage.xcdatamodel; sourceTree = ""; }; + F88C2481295C3A4F0006098B /* ImageDetailsModalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ImageDetailsModalView.swift; sourceTree = ""; }; + F88C2483295C3A870006098B /* DismissButtonView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DismissButtonView.swift; sourceTree = ""; }; + F88C2485295C48030006098B /* HTMLFotmattedText.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTMLFotmattedText.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -30,12 +41,55 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + F88C2480295C38400006098B /* MastodonSwift in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + F8341F93295C63E2009C8EE6 /* Views */ = { + isa = PBXGroup; + children = ( + F88C2483295C3A870006098B /* DismissButtonView.swift */, + F88C2481295C3A4F0006098B /* ImageDetailsModalView.swift */, + F88C246D295C37B80006098B /* MainView.swift */, + ); + path = Views; + sourceTree = ""; + }; + F8341F94295C63FE009C8EE6 /* Extensions */ = { + isa = PBXGroup; + children = ( + F8341F8F295C636C009C8EE6 /* UIImage.swift */, + ); + path = Extensions; + sourceTree = ""; + }; + F8341F95295C640C009C8EE6 /* Models */ = { + isa = PBXGroup; + children = ( + F8341F91295C63BB009C8EE6 /* ImageStatus.swift */, + ); + path = Models; + sourceTree = ""; + }; + F8341F96295C6427009C8EE6 /* CoreData */ = { + isa = PBXGroup; + children = ( + F88C2474295C37BB0006098B /* Persistence.swift */, + ); + path = CoreData; + sourceTree = ""; + }; + F8341F97295C6434009C8EE6 /* Formatters */ = { + isa = PBXGroup; + children = ( + F88C2485295C48030006098B /* HTMLFotmattedText.swift */, + ); + path = Formatters; + sourceTree = ""; + }; F88C245F295C37B80006098B = { isa = PBXGroup; children = ( @@ -55,10 +109,13 @@ F88C246A295C37B80006098B /* Vernissage */ = { isa = PBXGroup; children = ( + F8341F97295C6434009C8EE6 /* Formatters */, + F8341F96295C6427009C8EE6 /* CoreData */, + F8341F95295C640C009C8EE6 /* Models */, + F8341F94295C63FE009C8EE6 /* Extensions */, + F8341F93295C63E2009C8EE6 /* Views */, F88C246B295C37B80006098B /* VernissageApp.swift */, - F88C246D295C37B80006098B /* ContentView.swift */, F88C246F295C37BB0006098B /* Assets.xcassets */, - F88C2474295C37BB0006098B /* Persistence.swift */, F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */, F88C2471295C37BB0006098B /* Preview Content */, ); @@ -89,6 +146,9 @@ dependencies = ( ); name = Vernissage; + packageProductDependencies = ( + F88C247F295C38400006098B /* MastodonSwift */, + ); productName = Vernissage; productReference = F88C2468295C37B80006098B /* Vernissage.app */; productType = "com.apple.product-type.application"; @@ -117,6 +177,9 @@ Base, ); mainGroup = F88C245F295C37B80006098B; + packageReferences = ( + F88C247E295C38400006098B /* XCRemoteSwiftPackageReference "Mastodon" */, + ); productRefGroup = F88C2469295C37B80006098B /* Products */; projectDirPath = ""; projectRoot = ""; @@ -144,8 +207,13 @@ buildActionMask = 2147483647; files = ( F88C2475295C37BB0006098B /* Persistence.swift in Sources */, - F88C246E295C37B80006098B /* ContentView.swift in Sources */, + F8341F92295C63BB009C8EE6 /* ImageStatus.swift in Sources */, + F8341F90295C636C009C8EE6 /* UIImage.swift in Sources */, + F88C2484295C3A870006098B /* DismissButtonView.swift in Sources */, + F88C246E295C37B80006098B /* MainView.swift in Sources */, F88C2478295C37BB0006098B /* Vernissage.xcdatamodeld in Sources */, + F88C2482295C3A4F0006098B /* ImageDetailsModalView.swift in Sources */, + F88C2486295C48030006098B /* HTMLFotmattedText.swift in Sources */, F88C246C295C37B80006098B /* VernissageApp.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; @@ -283,6 +351,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -312,6 +381,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -348,6 +418,25 @@ }; /* End XCConfigurationList section */ +/* Begin XCRemoteSwiftPackageReference section */ + F88C247E295C38400006098B /* XCRemoteSwiftPackageReference "Mastodon" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/Swiftodon/Mastodon.swift"; + requirement = { + branch = main; + kind = branch; + }; + }; +/* End XCRemoteSwiftPackageReference section */ + +/* Begin XCSwiftPackageProductDependency section */ + F88C247F295C38400006098B /* MastodonSwift */ = { + isa = XCSwiftPackageProductDependency; + package = F88C247E295C38400006098B /* XCRemoteSwiftPackageReference "Mastodon" */; + productName = MastodonSwift; + }; +/* End XCSwiftPackageProductDependency section */ + /* Begin XCVersionGroup section */ F88C2476295C37BB0006098B /* Vernissage.xcdatamodeld */ = { isa = XCVersionGroup; diff --git a/Vernissage/Assets.xcassets/AppIcon.appiconset/Contents.json b/Vernissage/Assets.xcassets/AppIcon.appiconset/Contents.json index 13613e3..a657e33 100644 --- a/Vernissage/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/Vernissage/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "icon.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/Vernissage/Assets.xcassets/AppIcon.appiconset/icon.png b/Vernissage/Assets.xcassets/AppIcon.appiconset/icon.png new file mode 100644 index 0000000..5155859 Binary files /dev/null and b/Vernissage/Assets.xcassets/AppIcon.appiconset/icon.png differ diff --git a/Vernissage/Assets.xcassets/displayNameColor.colorset/Contents.json b/Vernissage/Assets.xcassets/displayNameColor.colorset/Contents.json new file mode 100644 index 0000000..d890719 --- /dev/null +++ b/Vernissage/Assets.xcassets/displayNameColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0.000", + "green" : "0.000", + "red" : "0.000" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "1.000", + "green" : "1.000", + "red" : "1.000" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/linkColor.colorset/Contents.json b/Vernissage/Assets.xcassets/linkColor.colorset/Contents.json new file mode 100644 index 0000000..5ff8429 --- /dev/null +++ b/Vernissage/Assets.xcassets/linkColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "143", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "164", + "red" : "0" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/mainTextColor.colorset/Contents.json b/Vernissage/Assets.xcassets/mainTextColor.colorset/Contents.json new file mode 100644 index 0000000..1592500 --- /dev/null +++ b/Vernissage/Assets.xcassets/mainTextColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "0", + "green" : "0", + "red" : "0" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "255", + "green" : "255", + "red" : "255" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/Assets.xcassets/userNameColor.colorset/Contents.json b/Vernissage/Assets.xcassets/userNameColor.colorset/Contents.json new file mode 100644 index 0000000..f07e8ce --- /dev/null +++ b/Vernissage/Assets.xcassets/userNameColor.colorset/Contents.json @@ -0,0 +1,38 @@ +{ + "colors" : [ + { + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "160", + "green" : "160", + "red" : "160" + } + }, + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "color" : { + "color-space" : "srgb", + "components" : { + "alpha" : "1.000", + "blue" : "160", + "green" : "160", + "red" : "160" + } + }, + "idiom" : "universal" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Vernissage/ContentView.swift b/Vernissage/ContentView.swift deleted file mode 100644 index ef1e237..0000000 --- a/Vernissage/ContentView.swift +++ /dev/null @@ -1,88 +0,0 @@ -// -// https://mczachurski.dev -// Copyright © 2022 Marcin Czachurski and the repository contributors. -// Licensed under the MIT License. -// - - -import SwiftUI -import CoreData - -struct ContentView: View { - @Environment(\.managedObjectContext) private var viewContext - - @FetchRequest( - sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], - animation: .default) - private var items: FetchedResults - - var body: some View { - NavigationView { - List { - ForEach(items) { item in - NavigationLink { - Text("Item at \(item.timestamp!, formatter: itemFormatter)") - } label: { - Text(item.timestamp!, formatter: itemFormatter) - } - } - .onDelete(perform: deleteItems) - } - .toolbar { - ToolbarItem(placement: .navigationBarTrailing) { - EditButton() - } - ToolbarItem { - Button(action: addItem) { - Label("Add Item", systemImage: "plus") - } - } - } - Text("Select an item") - } - } - - private func addItem() { - withAnimation { - let newItem = Item(context: viewContext) - newItem.timestamp = Date() - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } - - private func deleteItems(offsets: IndexSet) { - withAnimation { - offsets.map { items[$0] }.forEach(viewContext.delete) - - do { - try viewContext.save() - } catch { - // Replace this implementation with code to handle the error appropriately. - // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. - let nsError = error as NSError - fatalError("Unresolved error \(nsError), \(nsError.userInfo)") - } - } - } -} - -private let itemFormatter: DateFormatter = { - let formatter = DateFormatter() - formatter.dateStyle = .short - formatter.timeStyle = .medium - return formatter -}() - -struct ContentView_Previews: PreviewProvider { - static var previews: some View { - ContentView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) - } -} diff --git a/Vernissage/Persistence.swift b/Vernissage/CoreData/Persistence.swift similarity index 100% rename from Vernissage/Persistence.swift rename to Vernissage/CoreData/Persistence.swift diff --git a/Vernissage/Extensions/UIImage.swift b/Vernissage/Extensions/UIImage.swift new file mode 100644 index 0000000..bcb8475 --- /dev/null +++ b/Vernissage/Extensions/UIImage.swift @@ -0,0 +1,27 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import Foundation +import UIKit + +public extension UIImage { + func getExifData() -> CFDictionary? { + var exifData: CFDictionary? = nil + + if let data = self.jpegData(compressionQuality: 1.0) { + data.withUnsafeBytes { + let bytes = $0.baseAddress?.assumingMemoryBound(to: UInt8.self) + if let cfData = CFDataCreate(kCFAllocatorDefault, bytes, data.count), + let source = CGImageSourceCreateWithData(cfData, nil) { + exifData = CGImageSourceCopyPropertiesAtIndex(source, 0, nil) + } + } + } + + return exifData + } +} diff --git a/Vernissage/Formatters/HTMLFotmattedText.swift b/Vernissage/Formatters/HTMLFotmattedText.swift new file mode 100644 index 0000000..74f9722 --- /dev/null +++ b/Vernissage/Formatters/HTMLFotmattedText.swift @@ -0,0 +1,76 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import UIKit +import SwiftUI + +struct HTMLFormattedText: UIViewRepresentable { + + let text: String + private let textView = UITextView() + + init(_ content: String) { + self.text = content + } + + func makeUIView(context: UIViewRepresentableContext) -> UITextView { + textView.widthAnchor.constraint(equalToConstant:UIScreen.main.bounds.width - 16).isActive = true + textView.isSelectable = false + textView.isUserInteractionEnabled = false + textView.translatesAutoresizingMaskIntoConstraints = false + textView.isScrollEnabled = false + + return textView + } + + func updateUIView(_ uiView: UITextView, context: UIViewRepresentableContext) { + DispatchQueue.main.async { + if let attributeText = self.converHTML(text: text) { + textView.attributedText = attributeText + } else { + textView.text = "" + } + } + } + + private func converHTML(text: String) -> NSAttributedString?{ + guard let data = text.data(using: .utf8) else { + return nil + } + + let largeAttributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), + NSAttributedString.Key.foregroundColor: UIColor(Color("mainTextColor")) + ] + + let linkAttributes = [ + NSAttributedString.Key.font: UIFont.systemFont(ofSize: 16), + NSAttributedString.Key.foregroundColor: UIColor(Color("linkColor")) + ] + + if let attributedString = try? NSMutableAttributedString(data: data, options: [.documentType: NSAttributedString.DocumentType.html], documentAttributes: nil) { + + attributedString.enumerateAttributes(in: NSRange(0..Danish-made 1st class kebab

Say yes thanks to 2kg. delicious kebab, which is confused and cooked.

Yes thanks for 149.95

Now you can make the most delicious sandwiches, kebab mix and much more at home

") + } +} diff --git a/Vernissage/Models/ImageStatus.swift b/Vernissage/Models/ImageStatus.swift new file mode 100644 index 0000000..ba29463 --- /dev/null +++ b/Vernissage/Models/ImageStatus.swift @@ -0,0 +1,15 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import Foundation +import UIKit +import MastodonSwift + +public struct ImageStatus: Identifiable { + public let id: String + public let image: UIImage + public let status: Status +} diff --git a/Vernissage/VernissageApp.swift b/Vernissage/VernissageApp.swift index 6db0171..3fe2fc8 100644 --- a/Vernissage/VernissageApp.swift +++ b/Vernissage/VernissageApp.swift @@ -13,7 +13,7 @@ struct VernissageApp: App { var body: some Scene { WindowGroup { - ContentView() + MainView() .environment(\.managedObjectContext, persistenceController.container.viewContext) } } diff --git a/Vernissage/Views/DismissButtonView.swift b/Vernissage/Views/DismissButtonView.swift new file mode 100644 index 0000000..5c37b7b --- /dev/null +++ b/Vernissage/Views/DismissButtonView.swift @@ -0,0 +1,30 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI + +struct DismissButtonView: View { + var action: () -> () + + public init(_ action: @escaping () -> ()) { + self.action = action + } + + public var body: some View { + Button(action: action) { + RoundedRectangle(cornerRadius: 16) + .fill(Color.gray) + .frame(width: 50, height: 5) + } + } +} + +struct DismissButtonView_Previews: PreviewProvider { + static var previews: some View { + DismissButtonView { } + } +} diff --git a/Vernissage/Views/ImageDetailsModalView.swift b/Vernissage/Views/ImageDetailsModalView.swift new file mode 100644 index 0000000..ac2c686 --- /dev/null +++ b/Vernissage/Views/ImageDetailsModalView.swift @@ -0,0 +1,84 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + + +import SwiftUI +import MastodonSwift + +struct ImageDetailsModalView: View { + @Environment(\.dismiss) private var dismiss + @State public var current: ImageStatus + + var body: some View { + VStack { + DismissButtonView { dismiss() } + + ScrollView { + VStack (alignment: .leading) { + Image(uiImage: current.image) + .resizable().aspectRatio(contentMode: .fit) + .frame(maxWidth: .infinity) + .onTapGesture { + dismiss() + } + + VStack(alignment: .leading) { + HStack (alignment: .center) { + AsyncImage(url: current.status.account?.avatar) { image in + image + .resizable() + .clipShape(Circle()) + .shadow(radius: 10) + .aspectRatio(contentMode: .fit) + } placeholder: { + Color.gray + } + .frame(height: 60) + .frame(width: 60) + + VStack (alignment: .leading) { + Text(current.status.account?.displayName ?? current.status.account?.username ?? "") + .foregroundColor(Color("displayNameColor")) + Text("@\(current.status.account?.username ?? "unknown")") + .foregroundColor(Color("userNameColor")) + .font(.footnote) + } + .padding(.leading, 8) + } + + HTMLFormattedText(current.status.content) + + HStack (alignment: .top) { + Image(systemName: current.status.favourited ? "heart.fill" : "heart") + Text("\(current.status.favouritesCount) likes") + + Image(systemName: "arrow.2.squarepath") + .padding(.leading, 16) + Text("\(current.status.reblogsCount) boosts") + } + .font(.footnote) + .foregroundColor(Color("mainTextColor")) + } + .padding(8) + } + } + } + .gesture( + DragGesture().onEnded { value in + if value.location.y - value.startLocation.y > 150 { + dismiss() + } + } + ) + } +} + +struct FullScreenModalView_Previews: PreviewProvider { + static var previews: some View { + Text("") + // FullScreenModalView(current: ImageStatus(id: "123", image: UIImage(), status: Status(from: <#T##Decoder#>))) + } +} diff --git a/Vernissage/Views/MainView.swift b/Vernissage/Views/MainView.swift new file mode 100644 index 0000000..0b40d76 --- /dev/null +++ b/Vernissage/Views/MainView.swift @@ -0,0 +1,109 @@ +// +// https://mczachurski.dev +// Copyright © 2022 Marcin Czachurski and the repository contributors. +// Licensed under the MIT License. +// + +import SwiftUI +import UIKit +import CoreData +import MastodonSwift + +struct MainView: View { + @Environment(\.managedObjectContext) private var viewContext + + @FetchRequest( + sortDescriptors: [NSSortDescriptor(keyPath: \Item.timestamp, ascending: true)], + animation: .default) + private var items: FetchedResults + + @State private var statuses: [Status] = [] + @State private var images: [ImageStatus] = [] + @State private var showDetails: Bool = false + @State private var current: ImageStatus? = nil + + var body: some View { + ScrollView { + ForEach(images) { item in + Image(uiImage: item.image) + .resizable().aspectRatio(contentMode: .fit) + .onTapGesture { + current = item + showDetails.toggle() + } + .fullScreenCover(item: $current) { item in + ImageDetailsModalView(current: item) + } + } + } + .foregroundColor(Color.black) + .background(Color.black) + .edgesIgnoringSafeArea(.all) + .task { + do { + try await loadData() + } catch { + print("Error", error) + } + } + } + + private func loadData() async throws { + let accessToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiI2MTQwOCIsImp0aSI6IjZjMDg4N2ZlZThjZjBmZjQ1N2RjZDQ5MTU2YjE2NzYyYmQ2MDQ1NDQ1MmEwYzEyMzVmNDA3YjY2YjFhYjU3ZjBjMTY2YjFmZmIyNjJlZDg2IiwiaWF0IjoxNjcyMDU5MDYwLjI3NDgyMSwibmJmIjoxNjcyMDU5MDYwLjI3NDgyNCwiZXhwIjoxNzAzNTk1MDYwLjI1MzM1Nywic3ViIjoiNjc4MjMiLCJzY29wZXMiOlsicmVhZCIsIndyaXRlIiwiZm9sbG93Il19.kGvg3lW8lF1X1mOTdgGgoXNyzwUIJz5hz5RJKK_WiSoBWDQNadhZDty7XMNF0IAPjxOSi6UaIx2av7_eH_65aNlKFw89bkm8bT_zFQW2V0KbADJ-NmE6X0B_NgU2CNoF5IPn6bhCFHCKMtV6MWAQ_db6DT-LXaGemMY3QimcJzCqQuXI_1ouiZ235T297uEPNTrLwtLq-x_UoO-wx254LStBalDIGDVHAa4by9IT-mvu-QXz7k8pH2NHKoX-9Ql_Y3G9RJJNqoOmWMU45Dyo2HaJKKEb1tkeJ9tA3LIYgbwnEbG2PJ7CE8CXxtakiCIflJZpzzOmq1jXLAsCJ1mHnc77o7NfMaB_hY-f8PEI6d2ttOdH8bNlreF2avznNAIVHg_bf-yv_4wKUCUe0QZMG_yWqOwOk6lyruvboSGKuI5RnYsJbXBoJTGMLON6jVmtiKPbHy-9jNcfFgShAc3D5kTO-8Avj9_RquqEh1TQF_S4ljmganxKzMihyMDLK1OVcXzCFO6FKlCw7YKvbfJk1Qrn9kPBrVDM5jzIyXAmqRd1ivcE9nAdYb2l7KnxW_pi31uT0IdJMpTkZrUQSDMyEnj0HgV6Yd5BDlLG6Cnk8GXATTcU-a1pgE13OtWsCpD2cZQm-tOsFHWBDvY-BA0RtTvQAyEUxRIP9NjHe8rSR90" + + let client = MastodonClient(baseURL: URL(string: "https://pixelfed.social")!) + .getAuthenticated(token: accessToken) + + self.statuses = try await client.getHomeTimeline() + + var imagesCache: [ImageStatus] = [] + for item in self.statuses { + let imageStatus = try await self.fetchImage(status: item, accessToken: accessToken) + + if let imageStatus { + imagesCache.append(imageStatus) + } + } + + self.images = imagesCache + } + + public func fetchImage(status: Status, accessToken: String) async throws -> ImageStatus? { + guard let url = status.mediaAttachments.first?.url, let id = status.mediaAttachments.first?.id else { + return nil + } + + let urlRequest = URLRequest(url: url) + + // Fetching data. + let (data, response) = try await URLSession.shared.data(for: urlRequest) + guard (response as? HTTPURLResponse)?.statusCode == 200 else { + return nil + } + + // Decoding JSON. + let image = UIImage(data: data) + guard let image = image else { + return nil + } + + /* + var exif = image.getExifData() + if let dict = exif as? [String: AnyObject] { + dict.keys.map { key in + print(key) + print(dict[key]) + } + } + */ + + return ImageStatus(id: id,image: image, status: status) + } +} + + +struct MainView_Previews: PreviewProvider { + static var previews: some View { + MainView().environment(\.managedObjectContext, PersistenceController.preview.container.viewContext) + } +} diff --git a/icon.afdesign b/icon.afdesign new file mode 100644 index 0000000..826ea2d Binary files /dev/null and b/icon.afdesign differ