feat: implement image media status cell UI
This commit is contained in:
parent
cee84d95a0
commit
98ebddc438
@ -1,5 +1,5 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
|
||||||
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D5029f" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="17709" systemVersion="20D74" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
|
||||||
<entity name="Application" representedClassName=".Application" syncable="YES">
|
<entity name="Application" representedClassName=".Application" syncable="YES">
|
||||||
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
<attribute name="identifier" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
|
||||||
<attribute name="name" attributeType="String"/>
|
<attribute name="name" attributeType="String"/>
|
||||||
@ -7,6 +7,23 @@
|
|||||||
<attribute name="website" optional="YES" attributeType="String"/>
|
<attribute name="website" optional="YES" attributeType="String"/>
|
||||||
<relationship name="toots" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="application" inverseEntity="Toot"/>
|
<relationship name="toots" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Toot" inverseName="application" inverseEntity="Toot"/>
|
||||||
</entity>
|
</entity>
|
||||||
|
<entity name="Attachment" representedClassName=".Attachment" syncable="YES">
|
||||||
|
<attribute name="blurhash" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="createdAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="descriptionString" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="domain" attributeType="String"/>
|
||||||
|
<attribute name="id" attributeType="String"/>
|
||||||
|
<attribute name="index" attributeType="Integer 64" defaultValueString="0" usesScalarValueType="YES"/>
|
||||||
|
<attribute name="metaData" optional="YES" attributeType="Binary"/>
|
||||||
|
<attribute name="previewRemoteURL" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="previewURL" attributeType="String"/>
|
||||||
|
<attribute name="remoteURL" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="textURL" optional="YES" attributeType="String"/>
|
||||||
|
<attribute name="typeRaw" attributeType="String"/>
|
||||||
|
<attribute name="updatedAt" attributeType="Date" usesScalarValueType="NO"/>
|
||||||
|
<attribute name="url" optional="YES" attributeType="String"/>
|
||||||
|
<relationship name="toot" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="Toot" inverseName="mediaAttachments" inverseEntity="Toot"/>
|
||||||
|
</entity>
|
||||||
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
<entity name="Emoji" representedClassName=".Emoji" syncable="YES">
|
||||||
<attribute name="category" optional="YES" attributeType="String"/>
|
<attribute name="category" optional="YES" attributeType="String"/>
|
||||||
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
<attribute name="createAt" attributeType="Date" defaultDateTimeInterval="631123200" usesScalarValueType="NO"/>
|
||||||
@ -110,6 +127,7 @@
|
|||||||
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
<relationship name="emojis" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Emoji" inverseName="toot" inverseEntity="Emoji"/>
|
||||||
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
<relationship name="favouritedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="favourite" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toot" inverseEntity="HomeTimelineIndex"/>
|
<relationship name="homeTimelineIndexes" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="HomeTimelineIndex" inverseName="toot" inverseEntity="HomeTimelineIndex"/>
|
||||||
|
<relationship name="mediaAttachments" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Attachment" inverseName="toot" inverseEntity="Attachment"/>
|
||||||
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
|
<relationship name="mentions" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="Mention" inverseName="toot" inverseEntity="Mention"/>
|
||||||
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
<relationship name="mutedBy" optional="YES" toMany="YES" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="muted" inverseEntity="MastodonUser"/>
|
||||||
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedToot" inverseEntity="MastodonUser"/>
|
<relationship name="pinnedBy" optional="YES" maxCount="1" deletionRule="Nullify" destinationEntity="MastodonUser" inverseName="pinnedToot" inverseEntity="MastodonUser"/>
|
||||||
@ -127,6 +145,7 @@
|
|||||||
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
<element name="MastodonUser" positionX="0" positionY="0" width="128" height="284"/>
|
||||||
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
<element name="Mention" positionX="9" positionY="108" width="128" height="134"/>
|
||||||
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
<element name="Tag" positionX="18" positionY="117" width="128" height="119"/>
|
||||||
<element name="Toot" positionX="0" positionY="0" width="128" height="509"/>
|
<element name="Toot" positionX="0" positionY="0" width="128" height="524"/>
|
||||||
|
<element name="Attachment" positionX="72" positionY="162" width="128" height="14"/>
|
||||||
</elements>
|
</elements>
|
||||||
</model>
|
</model>
|
126
CoreDataStack/Entity/Attachment.swift
Normal file
126
CoreDataStack/Entity/Attachment.swift
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
//
|
||||||
|
// Attachment.swift
|
||||||
|
// CoreDataStack
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-2-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import CoreData
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
public final class Attachment: NSManagedObject {
|
||||||
|
public typealias ID = String
|
||||||
|
|
||||||
|
@NSManaged public private(set) var id: ID
|
||||||
|
@NSManaged public private(set) var domain: String
|
||||||
|
@NSManaged public private(set) var typeRaw: String
|
||||||
|
@NSManaged public private(set) var url: String
|
||||||
|
@NSManaged public private(set) var previewURL: String
|
||||||
|
|
||||||
|
@NSManaged public private(set) var remoteURL: String?
|
||||||
|
@NSManaged public private(set) var metaData: Data?
|
||||||
|
@NSManaged public private(set) var textURL: String?
|
||||||
|
@NSManaged public private(set) var descriptionString: String?
|
||||||
|
@NSManaged public private(set) var blurhash: String?
|
||||||
|
|
||||||
|
@NSManaged public private(set) var createdAt: Date
|
||||||
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
|
@NSManaged public private(set) var index: NSNumber
|
||||||
|
|
||||||
|
// many-to-one relastionship
|
||||||
|
@NSManaged public private(set) var toot: Toot?
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Attachment {
|
||||||
|
|
||||||
|
override func awakeFromInsert() {
|
||||||
|
super.awakeFromInsert()
|
||||||
|
createdAt = Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
@discardableResult
|
||||||
|
static func insert(
|
||||||
|
into context: NSManagedObjectContext,
|
||||||
|
property: Property
|
||||||
|
) -> Attachment {
|
||||||
|
let attachment: Attachment = context.insertObject()
|
||||||
|
|
||||||
|
attachment.domain = property.domain
|
||||||
|
attachment.index = property.index
|
||||||
|
|
||||||
|
attachment.id = property.id
|
||||||
|
attachment.typeRaw = property.typeRaw
|
||||||
|
attachment.url = property.url
|
||||||
|
attachment.previewURL = property.previewURL
|
||||||
|
|
||||||
|
attachment.remoteURL = property.remoteURL
|
||||||
|
attachment.metaData = property.metaData
|
||||||
|
attachment.textURL = property.textURL
|
||||||
|
attachment.descriptionString = property.descriptionString
|
||||||
|
attachment.blurhash = property.blurhash
|
||||||
|
|
||||||
|
attachment.updatedAt = property.networkDate
|
||||||
|
|
||||||
|
return attachment
|
||||||
|
}
|
||||||
|
|
||||||
|
func didUpdate(at networkDate: Date) {
|
||||||
|
self.updatedAt = networkDate
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public extension Attachment {
|
||||||
|
struct Property {
|
||||||
|
public let domain: String
|
||||||
|
public let index: NSNumber
|
||||||
|
|
||||||
|
public let id: ID
|
||||||
|
public let typeRaw: String
|
||||||
|
public let url: String
|
||||||
|
|
||||||
|
public let previewURL: String
|
||||||
|
public let remoteURL: String?
|
||||||
|
public let metaData: Data?
|
||||||
|
public let textURL: String?
|
||||||
|
public let descriptionString: String?
|
||||||
|
public let blurhash: String?
|
||||||
|
|
||||||
|
public let networkDate: Date
|
||||||
|
|
||||||
|
public init(
|
||||||
|
domain: String,
|
||||||
|
index: Int,
|
||||||
|
id: Attachment.ID,
|
||||||
|
typeRaw: String,
|
||||||
|
url: String,
|
||||||
|
previewURL: String,
|
||||||
|
remoteURL: String?,
|
||||||
|
metaData: Data?,
|
||||||
|
textURL: String?,
|
||||||
|
descriptionString: String?,
|
||||||
|
blurhash: String?,
|
||||||
|
networkDate: Date
|
||||||
|
) {
|
||||||
|
self.domain = domain
|
||||||
|
self.index = NSNumber(value: index)
|
||||||
|
self.id = id
|
||||||
|
self.typeRaw = typeRaw
|
||||||
|
self.url = url
|
||||||
|
self.previewURL = previewURL
|
||||||
|
self.remoteURL = remoteURL
|
||||||
|
self.metaData = metaData
|
||||||
|
self.textURL = textURL
|
||||||
|
self.descriptionString = descriptionString
|
||||||
|
self.blurhash = blurhash
|
||||||
|
self.networkDate = networkDate
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension Attachment: Managed {
|
||||||
|
public static var defaultSortDescriptors: [NSSortDescriptor] {
|
||||||
|
return [NSSortDescriptor(keyPath: \Attachment.createdAt, ascending: false)]
|
||||||
|
}
|
||||||
|
}
|
@ -39,6 +39,8 @@ public final class Toot: NSManagedObject {
|
|||||||
// many-to-one relastionship
|
// many-to-one relastionship
|
||||||
@NSManaged public private(set) var author: MastodonUser
|
@NSManaged public private(set) var author: MastodonUser
|
||||||
@NSManaged public private(set) var reblog: Toot?
|
@NSManaged public private(set) var reblog: Toot?
|
||||||
|
|
||||||
|
// many-to-many relastionship
|
||||||
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var favouritedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var rebloggedBy: Set<MastodonUser>?
|
||||||
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
@NSManaged public private(set) var mutedBy: Set<MastodonUser>?
|
||||||
@ -53,6 +55,7 @@ public final class Toot: NSManagedObject {
|
|||||||
@NSManaged public private(set) var emojis: Set<Emoji>?
|
@NSManaged public private(set) var emojis: Set<Emoji>?
|
||||||
@NSManaged public private(set) var tags: Set<Tag>?
|
@NSManaged public private(set) var tags: Set<Tag>?
|
||||||
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
@NSManaged public private(set) var homeTimelineIndexes: Set<HomeTimelineIndex>?
|
||||||
|
@NSManaged public private(set) var mediaAttachments: Set<Attachment>?
|
||||||
|
|
||||||
@NSManaged public private(set) var updatedAt: Date
|
@NSManaged public private(set) var updatedAt: Date
|
||||||
@NSManaged public private(set) var deletedAt: Date?
|
@NSManaged public private(set) var deletedAt: Date?
|
||||||
@ -69,6 +72,7 @@ public extension Toot {
|
|||||||
mentions: [Mention]?,
|
mentions: [Mention]?,
|
||||||
emojis: [Emoji]?,
|
emojis: [Emoji]?,
|
||||||
tags: [Tag]?,
|
tags: [Tag]?,
|
||||||
|
mediaAttachments: [Attachment]?,
|
||||||
favouritedBy: MastodonUser?,
|
favouritedBy: MastodonUser?,
|
||||||
rebloggedBy: MastodonUser?,
|
rebloggedBy: MastodonUser?,
|
||||||
mutedBy: MastodonUser?,
|
mutedBy: MastodonUser?,
|
||||||
@ -115,6 +119,9 @@ public extension Toot {
|
|||||||
if let tags = tags {
|
if let tags = tags {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags)
|
toot.mutableSetValue(forKey: #keyPath(Toot.tags)).addObjects(from: tags)
|
||||||
}
|
}
|
||||||
|
if let mediaAttachments = mediaAttachments {
|
||||||
|
toot.mutableSetValue(forKey: #keyPath(Toot.mediaAttachments)).addObjects(from: mediaAttachments)
|
||||||
|
}
|
||||||
if let favouritedBy = favouritedBy {
|
if let favouritedBy = favouritedBy {
|
||||||
toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy)
|
toot.mutableSetValue(forKey: #keyPath(Toot.favouritedBy)).add(favouritedBy)
|
||||||
}
|
}
|
||||||
|
@ -137,6 +137,10 @@
|
|||||||
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
DB9D6BE925E4F5340051B173 /* SearchViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BE825E4F5340051B173 /* SearchViewController.swift */; };
|
||||||
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
DB9D6BF825E4F5690051B173 /* NotificationViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */; };
|
||||||
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
DB9D6BFF25E4F5940051B173 /* ProfileViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */; };
|
||||||
|
DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */; };
|
||||||
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */; };
|
||||||
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C2D25E504AC0051B173 /* Attachment.swift */; };
|
||||||
|
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */ = {isa = PBXBuildFile; fileRef = DB9D6C3725E508BE0051B173 /* Attachment.swift */; };
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */; };
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */; };
|
||||||
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
DBE0822425CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */; };
|
||||||
@ -334,6 +338,10 @@
|
|||||||
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
DB9D6BE825E4F5340051B173 /* SearchViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SearchViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
DB9D6BF725E4F5690051B173 /* NotificationViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationViewController.swift; sourceTree = "<group>"; };
|
||||||
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
DB9D6BFE25E4F5940051B173 /* ProfileViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileViewController.swift; sourceTree = "<group>"; };
|
||||||
|
DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageView.swift; sourceTree = "<group>"; };
|
||||||
|
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MosaicImageViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
DB9D6C2D25E504AC0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
|
DB9D6C3725E508BE0051B173 /* Attachment.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Attachment.swift; sourceTree = "<group>"; };
|
||||||
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
DBD9148F25DF6D8D00903DFD /* APIService+Onboarding.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "APIService+Onboarding.swift"; sourceTree = "<group>"; };
|
||||||
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
DBE0821425CD382600FD6BBD /* MastodonRegisterViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewController.swift; sourceTree = "<group>"; };
|
||||||
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
DBE0822325CD3F1E00FD6BBD /* MastodonRegisterViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MastodonRegisterViewModel.swift; sourceTree = "<group>"; };
|
||||||
@ -547,6 +555,7 @@
|
|||||||
2D7631A425C1532200929FB9 /* Share */ = {
|
2D7631A425C1532200929FB9 /* Share */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
DB9D6C2025E502C60051B173 /* ViewModel */,
|
||||||
2D7631A525C1532D00929FB9 /* View */,
|
2D7631A525C1532D00929FB9 /* View */,
|
||||||
);
|
);
|
||||||
path = Share;
|
path = Share;
|
||||||
@ -557,6 +566,7 @@
|
|||||||
children = (
|
children = (
|
||||||
2D42FF8325C82245004A627A /* Button */,
|
2D42FF8325C82245004A627A /* Button */,
|
||||||
2D42FF7C25C82207004A627A /* ToolBar */,
|
2D42FF7C25C82207004A627A /* ToolBar */,
|
||||||
|
DB9D6C1325E4F97A0051B173 /* Container */,
|
||||||
2D152A8A25C295B8009AA50C /* Content */,
|
2D152A8A25C295B8009AA50C /* Content */,
|
||||||
2D7631A625C1533800929FB9 /* TableviewCell */,
|
2D7631A625C1533800929FB9 /* TableviewCell */,
|
||||||
);
|
);
|
||||||
@ -628,6 +638,7 @@
|
|||||||
children = (
|
children = (
|
||||||
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
|
DB45FAE225CA7181005A8AC7 /* MastodonUser.swift */,
|
||||||
DB084B5625CBC56C00F898ED /* Toot.swift */,
|
DB084B5625CBC56C00F898ED /* Toot.swift */,
|
||||||
|
DB9D6C3725E508BE0051B173 /* Attachment.swift */,
|
||||||
);
|
);
|
||||||
path = CoreDataStack;
|
path = CoreDataStack;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -816,6 +827,7 @@
|
|||||||
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
2D927F1325C7EDD9004F19B8 /* Emoji.swift */,
|
||||||
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
DB45FAEC25CA7A9A005A8AC7 /* MastodonAuthentication.swift */,
|
||||||
2DA7D05625CA693F00804E11 /* Application.swift */,
|
2DA7D05625CA693F00804E11 /* Application.swift */,
|
||||||
|
DB9D6C2D25E504AC0051B173 /* Attachment.swift */,
|
||||||
);
|
);
|
||||||
path = Entity;
|
path = Entity;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
@ -935,6 +947,22 @@
|
|||||||
path = Profile;
|
path = Profile;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
DB9D6C1325E4F97A0051B173 /* Container */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB9D6C0D25E4F9780051B173 /* MosaicImageView.swift */,
|
||||||
|
);
|
||||||
|
path = Container;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
DB9D6C2025E502C60051B173 /* ViewModel */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
DB9D6C2225E502C60051B173 /* MosaicImageViewModel.swift */,
|
||||||
|
);
|
||||||
|
path = ViewModel;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
DBE0821A25CD382900FD6BBD /* Register */ = {
|
DBE0821A25CD382900FD6BBD /* Register */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -1319,6 +1347,7 @@
|
|||||||
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
DB98338825C945ED00AD9700 /* Assets.swift in Sources */,
|
||||||
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
2DA7D04425CA52B200804E11 /* TimelineLoaderTableViewCell.swift in Sources */,
|
||||||
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
DB8AF52F25C13561002E6C99 /* DocumentStore.swift in Sources */,
|
||||||
|
DB9D6C2425E502C60051B173 /* MosaicImageViewModel.swift in Sources */,
|
||||||
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
2D38F1EB25CD477000561493 /* HomeTimelineViewModel+LoadLatestState.swift in Sources */,
|
||||||
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
DBD9149025DF6D8D00903DFD /* APIService+Onboarding.swift in Sources */,
|
||||||
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
DB98337F25C9452D00AD9700 /* APIService+APIError.swift in Sources */,
|
||||||
@ -1355,6 +1384,7 @@
|
|||||||
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
DB0140A825C40C1500F9F3CF /* MastodonPinBasedAuthenticationViewModelNavigationDelegateShim.swift in Sources */,
|
||||||
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
DB2B3AE925E38850007045F9 /* UIViewPreview.swift in Sources */,
|
||||||
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
DB427DD625BAA00100D1B89D /* AppDelegate.swift in Sources */,
|
||||||
|
DB9D6C0E25E4F9780051B173 /* MosaicImageView.swift in Sources */,
|
||||||
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
DB98338725C945ED00AD9700 /* Strings.swift in Sources */,
|
||||||
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
DB45FAB625CA5485005A8AC7 /* UIAlertController.swift in Sources */,
|
||||||
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
DBE0821525CD382600FD6BBD /* MastodonRegisterViewController.swift in Sources */,
|
||||||
@ -1369,6 +1399,7 @@
|
|||||||
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
DB98339C25C96DE600AD9700 /* APIService+Account.swift in Sources */,
|
||||||
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
2D42FF6B25C817D2004A627A /* MastodonContent.swift in Sources */,
|
||||||
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
2DF75BA725D10E1000694EC8 /* APIService+Favorite.swift in Sources */,
|
||||||
|
DB9D6C3825E508BE0051B173 /* Attachment.swift in Sources */,
|
||||||
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
DB8AF52E25C13561002E6C99 /* ViewStateStore.swift in Sources */,
|
||||||
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
2DA7D04A25CA52CB00804E11 /* TimelineBottomLoaderTableViewCell.swift in Sources */,
|
||||||
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
2D76318325C14E8F00929FB9 /* PublicTimelineViewModel+Diffable.swift in Sources */,
|
||||||
@ -1412,6 +1443,7 @@
|
|||||||
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */,
|
2DF75BC725D1475D00694EC8 /* ManagedObjectContextObjectsDidChange.swift in Sources */,
|
||||||
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
DB89BA1225C1105C008580ED /* CoreDataStack.swift in Sources */,
|
||||||
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
DB89BA1C25C1107F008580ED /* NSManagedObjectContext.swift in Sources */,
|
||||||
|
DB9D6C2E25E504AC0051B173 /* Attachment.swift in Sources */,
|
||||||
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
2D927F0E25C7E9C9004F19B8 /* History.swift in Sources */,
|
||||||
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
|
DB89BA3725C1145C008580ED /* CoreData.xcdatamodeld in Sources */,
|
||||||
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
|
DB8AF52525C131D1002E6C99 /* MastodonUser.swift in Sources */,
|
||||||
|
@ -34,7 +34,7 @@ extension TimelineSection {
|
|||||||
// configure cell
|
// configure cell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
let timelineIndex = managedObjectContext.object(with: objectID) as! HomeTimelineIndex
|
||||||
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID)
|
TimelineSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: timelineIndex.toot, requestUserID: timelineIndex.userID)
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
@ -45,7 +45,7 @@ extension TimelineSection {
|
|||||||
// configure cell
|
// configure cell
|
||||||
managedObjectContext.performAndWait {
|
managedObjectContext.performAndWait {
|
||||||
let toot = managedObjectContext.object(with: objectID) as! Toot
|
let toot = managedObjectContext.object(with: objectID) as! Toot
|
||||||
TimelineSection.configure(cell: cell, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID)
|
TimelineSection.configure(cell: cell, readableLayoutFrame: tableView.readableContentGuide.layoutFrame, timestampUpdatePublisher: timestampUpdatePublisher, toot: toot, requestUserID: requestUserID)
|
||||||
}
|
}
|
||||||
cell.delegate = timelinePostTableViewCellDelegate
|
cell.delegate = timelinePostTableViewCellDelegate
|
||||||
return cell
|
return cell
|
||||||
@ -69,22 +69,74 @@ extension TimelineSection {
|
|||||||
|
|
||||||
static func configure(
|
static func configure(
|
||||||
cell: StatusTableViewCell,
|
cell: StatusTableViewCell,
|
||||||
|
readableLayoutFrame: CGRect?,
|
||||||
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
timestampUpdatePublisher: AnyPublisher<Date, Never>,
|
||||||
toot: Toot,
|
toot: Toot,
|
||||||
requestUserID: String
|
requestUserID: String
|
||||||
) {
|
) {
|
||||||
// set header
|
// set header
|
||||||
cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil
|
cell.statusView.headerContainerStackView.isHidden = toot.reblog == nil
|
||||||
cell.statusView.headerInfoLabel.text = L10n.Common.Controls.Status.userboosted(toot.author.displayName)
|
cell.statusView.headerInfoLabel.text = {
|
||||||
|
let author = toot.author
|
||||||
|
let name = author.displayName.isEmpty ? author.username : author.displayName
|
||||||
|
return L10n.Common.Controls.Status.userboosted(name)
|
||||||
|
}()
|
||||||
|
|
||||||
// set name username avatar
|
// set name username avatar
|
||||||
cell.statusView.nameLabel.text = toot.author.displayName
|
cell.statusView.nameLabel.text = {
|
||||||
cell.statusView.usernameLabel.text = "@" + toot.author.acct
|
let author = (toot.reblog ?? toot).author
|
||||||
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: toot.author.avatarImageURL()))
|
return author.displayName.isEmpty ? author.username : author.displayName
|
||||||
|
}()
|
||||||
|
cell.statusView.usernameLabel.text = "@" + (toot.reblog ?? toot).author.acct
|
||||||
|
cell.statusView.configure(with: AvatarConfigurableViewConfiguration(avatarImageURL: (toot.reblog ?? toot).author.avatarImageURL()))
|
||||||
|
|
||||||
// set text
|
// set text
|
||||||
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
cell.statusView.activeTextLabel.config(content: (toot.reblog ?? toot).content)
|
||||||
|
|
||||||
|
// prepare media attachments
|
||||||
|
let mediaAttachments = Array((toot.reblog ?? toot).mediaAttachments ?? []).sorted { $0.index.compare($1.index) == .orderedAscending }
|
||||||
|
|
||||||
|
// set image
|
||||||
|
let mosiacImageViewModel = MosaicImageViewModel(mediaAttachments: mediaAttachments)
|
||||||
|
let imageViewMaxSize: CGSize = {
|
||||||
|
let maxWidth: CGFloat = {
|
||||||
|
// use timelinePostView width as container width
|
||||||
|
// that width follows readable width and keep constant width after rotate
|
||||||
|
let containerFrame = readableLayoutFrame ?? cell.statusView.frame
|
||||||
|
var containerWidth = containerFrame.width
|
||||||
|
containerWidth -= 10
|
||||||
|
containerWidth -= StatusView.avatarImageSize.width
|
||||||
|
return containerWidth
|
||||||
|
}()
|
||||||
|
let scale: CGFloat = {
|
||||||
|
switch mosiacImageViewModel.metas.count {
|
||||||
|
case 1: return 1.3
|
||||||
|
default: return 0.7
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return CGSize(width: maxWidth, height: maxWidth * scale)
|
||||||
|
}()
|
||||||
|
if mosiacImageViewModel.metas.count == 1 {
|
||||||
|
let meta = mosiacImageViewModel.metas[0]
|
||||||
|
let imageView = cell.statusView.mosaicImageView.setupImageView(aspectRatio: meta.size, maxSize: imageViewMaxSize)
|
||||||
|
imageView.af.setImage(
|
||||||
|
withURL: meta.url,
|
||||||
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
imageTransition: .crossDissolve(0.2)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let imageViews = cell.statusView.mosaicImageView.setupImageViews(count: mosiacImageViewModel.metas.count, maxHeight: imageViewMaxSize.height)
|
||||||
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
let meta = mosiacImageViewModel.metas[i]
|
||||||
|
imageView.af.setImage(
|
||||||
|
withURL: meta.url,
|
||||||
|
placeholderImage: UIImage.placeholder(color: .systemFill),
|
||||||
|
imageTransition: .crossDissolve(0.2)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cell.statusView.mosaicImageView.isHidden = mosiacImageViewModel.metas.isEmpty
|
||||||
|
|
||||||
// toolbar
|
// toolbar
|
||||||
let replyCountTitle: String = {
|
let replyCountTitle: String = {
|
||||||
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
|
let count = (toot.reblog ?? toot).repliesCount?.intValue ?? 0
|
||||||
|
@ -41,10 +41,12 @@ extension ActiveLabel {
|
|||||||
|
|
||||||
extension ActiveLabel {
|
extension ActiveLabel {
|
||||||
func config(content: String) {
|
func config(content: String) {
|
||||||
if let parseResult = try? TootContent.parse(toot: content) {
|
|
||||||
activeEntities.removeAll()
|
activeEntities.removeAll()
|
||||||
|
if let parseResult = try? TootContent.parse(toot: content) {
|
||||||
text = parseResult.trimmed
|
text = parseResult.trimmed
|
||||||
activeEntities = parseResult.activeEntities
|
activeEntities = parseResult.activeEntities
|
||||||
|
} else {
|
||||||
|
text = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
23
Mastodon/Extension/CoreDataStack/Attachment.swift
Normal file
23
Mastodon/Extension/CoreDataStack/Attachment.swift
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
//
|
||||||
|
// Attachment.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by MainasuK Cirno on 2021-2-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import CoreDataStack
|
||||||
|
import MastodonSDK
|
||||||
|
|
||||||
|
extension Attachment {
|
||||||
|
|
||||||
|
var type: Mastodon.Entity.Attachment.AttachmentType {
|
||||||
|
return Mastodon.Entity.Attachment.AttachmentType(rawValue: typeRaw) ?? ._other(typeRaw)
|
||||||
|
}
|
||||||
|
|
||||||
|
var meta: Mastodon.Entity.Attachment.Meta? {
|
||||||
|
let decoder = JSONDecoder()
|
||||||
|
return metaData.flatMap { try? decoder.decode(Mastodon.Entity.Attachment.Meta.self, from: $0) }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
12
Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Preview Assets.xcassets/bradley-dunn.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "bradley-dunn-miqbDWtOG-o-unsplash.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 53 KiB |
12
Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Preview Assets.xcassets/lucas-ludwig.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "lucas-ludwig-8ARg12PU8nE-unsplash.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 116 KiB |
12
Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Preview Assets.xcassets/markus-spiske.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "markus-spiske-45R3oFOJt2k-unsplash.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
Binary file not shown.
After Width: | Height: | Size: 217 KiB |
12
Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/Contents.json
vendored
Normal file
12
Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/Contents.json
vendored
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "mrdongok-Z53ognhPjek-unsplash.jpg",
|
||||||
|
"idiom" : "universal"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/mrdongok-Z53ognhPjek-unsplash.jpg
vendored
Normal file
BIN
Mastodon/Resources/Preview Assets.xcassets/mrdongok.imageset/mrdongok-Z53ognhPjek-unsplash.jpg
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 128 KiB |
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 89 KiB |
284
Mastodon/Scene/Share/View/Container/MosaicImageView.swift
Normal file
284
Mastodon/Scene/Share/View/Container/MosaicImageView.swift
Normal file
@ -0,0 +1,284 @@
|
|||||||
|
//
|
||||||
|
// MosaicImageView.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by Cirno MainasuK on 2021-2-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import os.log
|
||||||
|
import func AVFoundation.AVMakeRect
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
protocol MosaicImageViewPresentable: class {
|
||||||
|
var mosaicImageView: MosaicImageView { get }
|
||||||
|
}
|
||||||
|
|
||||||
|
protocol MosaicImageViewDelegate: class {
|
||||||
|
func mosaicImageView(_ mosaicImageView: MosaicImageView, didTapImageView imageView: UIImageView, atIndex index: Int)
|
||||||
|
}
|
||||||
|
|
||||||
|
final class MosaicImageView: UIView {
|
||||||
|
|
||||||
|
static let cornerRadius: CGFloat = 4
|
||||||
|
|
||||||
|
weak var delegate: MosaicImageViewDelegate?
|
||||||
|
|
||||||
|
let container = UIStackView()
|
||||||
|
var imageViews = [UIImageView]() {
|
||||||
|
didSet {
|
||||||
|
imageViews.forEach { imageView in
|
||||||
|
imageView.isUserInteractionEnabled = true
|
||||||
|
let tapGesture = UITapGestureRecognizer.singleTapGestureRecognizer
|
||||||
|
tapGesture.addTarget(self, action: #selector(MosaicImageView.photoTapGestureRecognizerHandler(_:)))
|
||||||
|
imageView.addGestureRecognizer(tapGesture)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private var containerHeightLayoutConstraint: NSLayoutConstraint!
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
super.init(coder: coder)
|
||||||
|
_init()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MosaicImageView {
|
||||||
|
|
||||||
|
private func _init() {
|
||||||
|
container.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
addSubview(container)
|
||||||
|
containerHeightLayoutConstraint = container.heightAnchor.constraint(equalToConstant: 162).priority(.required - 1)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
container.topAnchor.constraint(equalTo: topAnchor),
|
||||||
|
container.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
trailingAnchor.constraint(equalTo: container.trailingAnchor),
|
||||||
|
bottomAnchor.constraint(equalTo: container.bottomAnchor),
|
||||||
|
containerHeightLayoutConstraint
|
||||||
|
])
|
||||||
|
|
||||||
|
container.axis = .horizontal
|
||||||
|
container.distribution = .fillEqually
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MosaicImageView {
|
||||||
|
|
||||||
|
func reset() {
|
||||||
|
container.arrangedSubviews.forEach { subview in
|
||||||
|
container.removeArrangedSubview(subview)
|
||||||
|
subview.removeFromSuperview()
|
||||||
|
}
|
||||||
|
container.subviews.forEach { subview in
|
||||||
|
subview.removeFromSuperview()
|
||||||
|
}
|
||||||
|
imageViews = []
|
||||||
|
|
||||||
|
container.spacing = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupImageView(aspectRatio: CGSize, maxSize: CGSize) -> UIImageView {
|
||||||
|
reset()
|
||||||
|
|
||||||
|
let contentView = UIView()
|
||||||
|
contentView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
container.addArrangedSubview(contentView)
|
||||||
|
|
||||||
|
let rect = AVMakeRect(
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
|
insideRect: CGRect(origin: .zero, size: maxSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
let imageView = UIImageView()
|
||||||
|
imageViews.append(imageView)
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
imageView.layer.cornerRadius = MosaicImageView.cornerRadius
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
|
||||||
|
imageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
contentView.addSubview(imageView)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
imageView.topAnchor.constraint(equalTo: contentView.topAnchor),
|
||||||
|
imageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor),
|
||||||
|
imageView.bottomAnchor.constraint(equalTo: contentView.bottomAnchor),
|
||||||
|
imageView.widthAnchor.constraint(equalToConstant: floor(rect.width)).priority(.required - 1),
|
||||||
|
])
|
||||||
|
containerHeightLayoutConstraint.constant = floor(rect.height)
|
||||||
|
containerHeightLayoutConstraint.isActive = true
|
||||||
|
|
||||||
|
return imageView
|
||||||
|
}
|
||||||
|
|
||||||
|
func setupImageViews(count: Int, maxHeight: CGFloat) -> [UIImageView] {
|
||||||
|
reset()
|
||||||
|
guard count > 1 else {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
|
containerHeightLayoutConstraint.constant = maxHeight
|
||||||
|
containerHeightLayoutConstraint.isActive = true
|
||||||
|
|
||||||
|
let contentLeftStackView = UIStackView()
|
||||||
|
let contentRightStackView = UIStackView()
|
||||||
|
[contentLeftStackView, contentRightStackView].forEach { stackView in
|
||||||
|
stackView.axis = .vertical
|
||||||
|
stackView.distribution = .fillEqually
|
||||||
|
stackView.spacing = 1
|
||||||
|
}
|
||||||
|
container.addArrangedSubview(contentLeftStackView)
|
||||||
|
container.addArrangedSubview(contentRightStackView)
|
||||||
|
|
||||||
|
var imageViews: [UIImageView] = []
|
||||||
|
for _ in 0..<count {
|
||||||
|
imageViews.append(UIImageView())
|
||||||
|
}
|
||||||
|
self.imageViews.append(contentsOf: imageViews)
|
||||||
|
imageViews.forEach { imageView in
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
imageView.layer.cornerRadius = MosaicImageView.cornerRadius
|
||||||
|
imageView.layer.cornerCurve = .continuous
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
}
|
||||||
|
if count == 2 {
|
||||||
|
contentLeftStackView.addArrangedSubview(imageViews[0])
|
||||||
|
contentRightStackView.addArrangedSubview(imageViews[1])
|
||||||
|
switch UIApplication.shared.userInterfaceLayoutDirection {
|
||||||
|
case .rightToLeft:
|
||||||
|
imageViews[1].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||||
|
imageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||||
|
default:
|
||||||
|
imageViews[0].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||||
|
imageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||||
|
}
|
||||||
|
|
||||||
|
} else if count == 3 {
|
||||||
|
contentLeftStackView.addArrangedSubview(imageViews[0])
|
||||||
|
contentRightStackView.addArrangedSubview(imageViews[1])
|
||||||
|
contentRightStackView.addArrangedSubview(imageViews[2])
|
||||||
|
switch UIApplication.shared.userInterfaceLayoutDirection {
|
||||||
|
case .rightToLeft:
|
||||||
|
imageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner, .layerMaxXMaxYCorner]
|
||||||
|
imageViews[1].layer.maskedCorners = [.layerMinXMinYCorner]
|
||||||
|
imageViews[2].layer.maskedCorners = [.layerMinXMaxYCorner]
|
||||||
|
default:
|
||||||
|
imageViews[0].layer.maskedCorners = [.layerMinXMinYCorner, .layerMinXMaxYCorner]
|
||||||
|
imageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner]
|
||||||
|
imageViews[2].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
||||||
|
}
|
||||||
|
} else if count == 4 {
|
||||||
|
contentLeftStackView.addArrangedSubview(imageViews[0])
|
||||||
|
contentRightStackView.addArrangedSubview(imageViews[1])
|
||||||
|
contentLeftStackView.addArrangedSubview(imageViews[2])
|
||||||
|
contentRightStackView.addArrangedSubview(imageViews[3])
|
||||||
|
switch UIApplication.shared.userInterfaceLayoutDirection {
|
||||||
|
case .rightToLeft:
|
||||||
|
imageViews[0].layer.maskedCorners = [.layerMaxXMinYCorner]
|
||||||
|
imageViews[1].layer.maskedCorners = [.layerMinXMinYCorner]
|
||||||
|
imageViews[2].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
||||||
|
imageViews[3].layer.maskedCorners = [.layerMinXMaxYCorner]
|
||||||
|
default:
|
||||||
|
imageViews[0].layer.maskedCorners = [.layerMinXMinYCorner]
|
||||||
|
imageViews[1].layer.maskedCorners = [.layerMaxXMinYCorner]
|
||||||
|
imageViews[2].layer.maskedCorners = [.layerMinXMaxYCorner]
|
||||||
|
imageViews[3].layer.maskedCorners = [.layerMaxXMaxYCorner]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return imageViews
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
extension MosaicImageView {
|
||||||
|
|
||||||
|
@objc private func photoTapGestureRecognizerHandler(_ sender: UITapGestureRecognizer) {
|
||||||
|
guard let imageView = sender.view as? UIImageView else { return }
|
||||||
|
guard let index = imageViews.firstIndex(of: imageView) else { return }
|
||||||
|
os_log(.info, log: .debug, "%{public}s[%{public}ld], %{public}s: tap photo at index: %ld", ((#file as NSString).lastPathComponent), #line, #function, index)
|
||||||
|
delegate?.mosaicImageView(self, didTapImageView: imageView, atIndex: index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#if DEBUG && canImport(SwiftUI)
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct MosaicImageView_Previews: PreviewProvider {
|
||||||
|
|
||||||
|
static var images: [UIImage] {
|
||||||
|
return ["bradley-dunn", "mrdongok", "lucas-ludwig", "markus-spiske"]
|
||||||
|
.map { UIImage(named: $0)! }
|
||||||
|
}
|
||||||
|
|
||||||
|
static var previews: some View {
|
||||||
|
Group {
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = MosaicImageView()
|
||||||
|
let image = images[3]
|
||||||
|
let imageView = view.setupImageView(
|
||||||
|
aspectRatio: image.size,
|
||||||
|
maxSize: CGSize(width: 375, height: 400)
|
||||||
|
)
|
||||||
|
imageView.image = image
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
.previewDisplayName("Portrait - one image")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = MosaicImageView()
|
||||||
|
let image = images[1]
|
||||||
|
let imageView = view.setupImageView(
|
||||||
|
aspectRatio: image.size,
|
||||||
|
maxSize: CGSize(width: 375, height: 400)
|
||||||
|
)
|
||||||
|
imageView.layer.masksToBounds = true
|
||||||
|
imageView.layer.cornerRadius = 8
|
||||||
|
imageView.contentMode = .scaleAspectFill
|
||||||
|
imageView.image = image
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 400))
|
||||||
|
.previewDisplayName("Landscape - one image")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = MosaicImageView()
|
||||||
|
let images = self.images.prefix(2)
|
||||||
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
imageView.image = images[i]
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("two image")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = MosaicImageView()
|
||||||
|
let images = self.images.prefix(3)
|
||||||
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
imageView.image = images[i]
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("three image")
|
||||||
|
UIViewPreview(width: 375) {
|
||||||
|
let view = MosaicImageView()
|
||||||
|
let images = self.images.prefix(4)
|
||||||
|
let imageViews = view.setupImageViews(count: images.count, maxHeight: 162)
|
||||||
|
for (i, imageView) in imageViews.enumerated() {
|
||||||
|
imageView.image = images[i]
|
||||||
|
}
|
||||||
|
return view
|
||||||
|
}
|
||||||
|
.previewLayout(.fixed(width: 375, height: 200))
|
||||||
|
.previewDisplayName("four image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
#endif
|
@ -73,6 +73,8 @@ final class StatusView: UIView {
|
|||||||
|
|
||||||
let statusContainerStackView = UIStackView()
|
let statusContainerStackView = UIStackView()
|
||||||
|
|
||||||
|
let mosaicImageView = MosaicImageView()
|
||||||
|
|
||||||
let actionToolbarContainer: ActionToolbarContainer = {
|
let actionToolbarContainer: ActionToolbarContainer = {
|
||||||
let actionToolbarContainer = ActionToolbarContainer()
|
let actionToolbarContainer = ActionToolbarContainer()
|
||||||
actionToolbarContainer.configure(for: .inline)
|
actionToolbarContainer.configure(for: .inline)
|
||||||
@ -183,12 +185,14 @@ extension StatusView {
|
|||||||
statusContainerStackView.spacing = 10
|
statusContainerStackView.spacing = 10
|
||||||
statusContainerStackView.addArrangedSubview(activeTextLabel)
|
statusContainerStackView.addArrangedSubview(activeTextLabel)
|
||||||
activeTextLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
activeTextLabel.setContentCompressionResistancePriority(.required - 2, for: .vertical)
|
||||||
|
statusContainerStackView.addArrangedSubview(mosaicImageView)
|
||||||
|
|
||||||
// action toolbar container
|
// action toolbar container
|
||||||
containerStackView.addArrangedSubview(actionToolbarContainer)
|
containerStackView.addArrangedSubview(actionToolbarContainer)
|
||||||
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
actionToolbarContainer.setContentCompressionResistancePriority(.defaultLow, for: .vertical)
|
||||||
|
|
||||||
headerContainerStackView.isHidden = true
|
headerContainerStackView.isHidden = true
|
||||||
|
mosaicImageView.isHidden = true
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -208,7 +212,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct StatusView_Previews: PreviewProvider {
|
struct StatusView_Previews: PreviewProvider {
|
||||||
|
|
||||||
static let avatarFlora = UIImage(named: "tiraya-adam-QfHEWqPelsc-unsplash")
|
static let avatarFlora = UIImage(named: "tiraya-adam")
|
||||||
|
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
Group {
|
Group {
|
||||||
|
36
Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift
Normal file
36
Mastodon/Scene/Share/ViewModel/MosaicImageViewModel.swift
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
//
|
||||||
|
// MosaicImageViewModel.swift
|
||||||
|
// Mastodon
|
||||||
|
//
|
||||||
|
// Created by Cirno MainasuK on 2021-2-23.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
import CoreDataStack
|
||||||
|
|
||||||
|
struct MosaicImageViewModel {
|
||||||
|
|
||||||
|
let metas: [MosaicMeta]
|
||||||
|
|
||||||
|
init(mediaAttachments: [Attachment]) {
|
||||||
|
var metas: [MosaicMeta] = []
|
||||||
|
for element in mediaAttachments where element.type == .image {
|
||||||
|
// Display original on the iPad/Mac
|
||||||
|
let urlString = UIDevice.current.userInterfaceIdiom == .phone ? element.previewURL : element.url
|
||||||
|
guard let meta = element.meta,
|
||||||
|
let width = meta.original?.width,
|
||||||
|
let height = meta.original?.height,
|
||||||
|
let url = URL(string: urlString) else {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
metas.append(MosaicMeta(url: url, size: CGSize(width: width, height: height)))
|
||||||
|
}
|
||||||
|
self.metas = metas
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
struct MosaicMeta {
|
||||||
|
let url: URL
|
||||||
|
let size: CGSize
|
||||||
|
}
|
@ -58,11 +58,24 @@ extension APIService.CoreData {
|
|||||||
Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
|
Emoji.insert(into: managedObjectContext, property: Emoji.Property(shortcode: emoji.shortcode, url: emoji.url, staticURL: emoji.staticURL, visibleInPicker: emoji.visibleInPicker, category: emoji.category))
|
||||||
}
|
}
|
||||||
let tags = entity.tags?.compactMap { tag -> Tag in
|
let tags = entity.tags?.compactMap { tag -> Tag in
|
||||||
let histories = tag.history?.compactMap({ (history) -> History in
|
let histories = tag.history?.compactMap { history -> History in
|
||||||
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
|
History.insert(into: managedObjectContext, property: History.Property(day: history.day, uses: history.uses, accounts: history.accounts))
|
||||||
})
|
}
|
||||||
return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories))
|
return Tag.insert(into: managedObjectContext, property: Tag.Property(name: tag.name, url: tag.url, histories: histories))
|
||||||
}
|
}
|
||||||
|
let mediaAttachments: [Attachment]? = {
|
||||||
|
let encoder = JSONEncoder()
|
||||||
|
var attachments: [Attachment] = []
|
||||||
|
for (index, attachment) in (entity.mediaAttachments ?? []).enumerated() {
|
||||||
|
let metaData = attachment.meta.flatMap { meta in
|
||||||
|
try? encoder.encode(meta)
|
||||||
|
}
|
||||||
|
let property = Attachment.Property(domain: domain, index: index, id: attachment.id, typeRaw: attachment.type.rawValue, url: attachment.url, previewURL: attachment.previewURL, remoteURL: attachment.remoteURL, metaData: metaData, textURL: attachment.textURL, descriptionString: attachment.description, blurhash: attachment.blurhash, networkDate: networkDate)
|
||||||
|
attachments.append(Attachment.insert(into: managedObjectContext, property: property))
|
||||||
|
}
|
||||||
|
guard !attachments.isEmpty else { return nil }
|
||||||
|
return attachments
|
||||||
|
}()
|
||||||
let tootProperty = Toot.Property(entity: entity, domain: domain, networkDate: networkDate)
|
let tootProperty = Toot.Property(entity: entity, domain: domain, networkDate: networkDate)
|
||||||
let toot = Toot.insert(
|
let toot = Toot.insert(
|
||||||
into: managedObjectContext,
|
into: managedObjectContext,
|
||||||
@ -73,6 +86,7 @@ extension APIService.CoreData {
|
|||||||
mentions: metions,
|
mentions: metions,
|
||||||
emojis: emojis,
|
emojis: emojis,
|
||||||
tags: tags,
|
tags: tags,
|
||||||
|
mediaAttachments: mediaAttachments,
|
||||||
favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil,
|
favouritedBy: (entity.favourited ?? false) ? requestMastodonUser : nil,
|
||||||
rebloggedBy: (entity.reblogged ?? false) ? requestMastodonUser : nil,
|
rebloggedBy: (entity.reblogged ?? false) ? requestMastodonUser : nil,
|
||||||
mutedBy: (entity.muted ?? false) ? requestMastodonUser : nil,
|
mutedBy: (entity.muted ?? false) ? requestMastodonUser : nil,
|
||||||
|
@ -47,6 +47,7 @@ extension Mastodon.Entity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
extension Mastodon.Entity.Attachment {
|
extension Mastodon.Entity.Attachment {
|
||||||
|
public typealias AttachmentType = Type
|
||||||
public enum `Type`: RawRepresentable, Codable {
|
public enum `Type`: RawRepresentable, Codable {
|
||||||
case unknown
|
case unknown
|
||||||
case image
|
case image
|
||||||
|
@ -14,7 +14,7 @@ extension Mastodon.Entity {
|
|||||||
/// - Since: 0.1.0
|
/// - Since: 0.1.0
|
||||||
/// - Version: 3.3.0
|
/// - Version: 3.3.0
|
||||||
/// # Last Update
|
/// # Last Update
|
||||||
/// 2021/1/28
|
/// 2021/2/23
|
||||||
/// # Reference
|
/// # Reference
|
||||||
/// [Document](https://docs.joinmastodon.org/entities/status/)
|
/// [Document](https://docs.joinmastodon.org/entities/status/)
|
||||||
public class Status: Codable {
|
public class Status: Codable {
|
||||||
@ -31,7 +31,7 @@ extension Mastodon.Entity {
|
|||||||
public let visibility: Visibility?
|
public let visibility: Visibility?
|
||||||
public let sensitive: Bool?
|
public let sensitive: Bool?
|
||||||
public let spoilerText: String?
|
public let spoilerText: String?
|
||||||
public let mediaAttachments: [Attachment]
|
public let mediaAttachments: [Attachment]?
|
||||||
public let application: Application?
|
public let application: Application?
|
||||||
|
|
||||||
// Rendering
|
// Rendering
|
||||||
|
Loading…
x
Reference in New Issue
Block a user