Compare commits
6 Commits
eb9a5aeb42
...
514e569bd5
Author | SHA1 | Date |
---|---|---|
Shadowfacts | 514e569bd5 | |
Shadowfacts | a22059a1a1 | |
Shadowfacts | 2cfefc9432 | |
Shadowfacts | 2f7c7bae5e | |
Shadowfacts | 3f04d74dd6 | |
Shadowfacts | 4dd8c1d692 |
|
@ -235,6 +235,7 @@
|
||||||
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
D6B81F3C2560365300F6E31D /* RefreshableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */; };
|
||||||
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
D6B81F442560390300F6E31D /* MenuController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B81F432560390300F6E31D /* MenuController.swift */; };
|
||||||
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
D6B8DB342182A59300424AF7 /* UIAlertController+Visibility.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */; };
|
||||||
|
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */; };
|
||||||
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
D6BC8748219738E1006163F1 /* EnhancedTableViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */; };
|
||||||
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
D6BC9DB1232C61BC002CA326 /* NotificationsPageViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */; };
|
||||||
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
D6BC9DB3232D4C07002CA326 /* WellnessPrefsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */; };
|
||||||
|
@ -287,6 +288,7 @@
|
||||||
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
D6E426AD25334DA500C02E1C /* FuzzyMatcherTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426AC25334DA500C02E1C /* FuzzyMatcherTests.swift */; };
|
||||||
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
D6E426B325337C7000C02E1C /* CustomEmojiImageView.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */; };
|
||||||
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
D6E57FA325C26FAB00341037 /* Localizable.stringsdict in Resources */ = {isa = PBXBuildFile; fileRef = D6E57FA525C26FAB00341037 /* Localizable.stringsdict */; };
|
||||||
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6E9CDA7281A427800BBC98E /* PostService.swift */; };
|
||||||
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
D6EAE0DB2550CC8A002DB0AC /* FocusableTextField.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */; };
|
||||||
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
D6EBF01523C55C0900AE061B /* UIApplication+Scenes.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */; };
|
||||||
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
D6EBF01723C55E0D00AE061B /* UISceneSession+MastodonController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */; };
|
||||||
|
@ -574,6 +576,7 @@
|
||||||
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
D6B81F3B2560365300F6E31D /* RefreshableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RefreshableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
D6B81F432560390300F6E31D /* MenuController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MenuController.swift; sourceTree = "<group>"; };
|
||||||
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
D6B8DB332182A59300424AF7 /* UIAlertController+Visibility.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIAlertController+Visibility.swift"; sourceTree = "<group>"; };
|
||||||
|
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainSidebarMyProfileCollectionViewCell.swift; sourceTree = "<group>"; };
|
||||||
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
D6BC8747219738E1006163F1 /* EnhancedTableViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EnhancedTableViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
D6BC9DB0232C61BC002CA326 /* NotificationsPageViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsPageViewController.swift; sourceTree = "<group>"; };
|
||||||
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
D6BC9DB2232D4C07002CA326 /* WellnessPrefsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WellnessPrefsView.swift; sourceTree = "<group>"; };
|
||||||
|
@ -635,6 +638,7 @@
|
||||||
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
D6E426B225337C7000C02E1C /* CustomEmojiImageView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CustomEmojiImageView.swift; sourceTree = "<group>"; };
|
||||||
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
D6E4885C24A2890C0011C13E /* Tusker.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = Tusker.entitlements; sourceTree = "<group>"; };
|
||||||
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
D6E57FA425C26FAB00341037 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.stringsdict; name = en; path = en.lproj/Localizable.stringsdict; sourceTree = "<group>"; };
|
||||||
|
D6E9CDA7281A427800BBC98E /* PostService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PostService.swift; sourceTree = "<group>"; };
|
||||||
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
D6EAE0DA2550CC8A002DB0AC /* FocusableTextField.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FocusableTextField.swift; sourceTree = "<group>"; };
|
||||||
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
D6EBF01423C55C0900AE061B /* UIApplication+Scenes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UIApplication+Scenes.swift"; sourceTree = "<group>"; };
|
||||||
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
D6EBF01623C55E0D00AE061B /* UISceneSession+MastodonController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "UISceneSession+MastodonController.swift"; sourceTree = "<group>"; };
|
||||||
|
@ -891,6 +895,7 @@
|
||||||
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
D6F0B12A24A3071C001E48C3 /* MainSplitViewController.swift */,
|
||||||
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
04DACE8B212CB14B009840C4 /* MainTabBarViewController.swift */,
|
||||||
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
D6F0B17424A3A1AA001E48C3 /* MainSidebarViewController.swift */,
|
||||||
|
D6B93666281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift */,
|
||||||
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
D68E525A24A3D77E0054355A /* TuskerRootViewController.swift */,
|
||||||
);
|
);
|
||||||
path = Main;
|
path = Main;
|
||||||
|
@ -1366,6 +1371,7 @@
|
||||||
D61959D2241E846D00A37B8E /* Models */,
|
D61959D2241E846D00A37B8E /* Models */,
|
||||||
D663626021360A9600C9CBA2 /* Preferences */,
|
D663626021360A9600C9CBA2 /* Preferences */,
|
||||||
D641C780213DD7C4004B4513 /* Screens */,
|
D641C780213DD7C4004B4513 /* Screens */,
|
||||||
|
D6E9CDA6281A426700BBC98E /* Services */,
|
||||||
D62D241E217AA46B005076CC /* Shortcuts */,
|
D62D241E217AA46B005076CC /* Shortcuts */,
|
||||||
D67B506B250B28FF00FAECFB /* Vendor */,
|
D67B506B250B28FF00FAECFB /* Vendor */,
|
||||||
D6BED1722126661300F02DA0 /* Views */,
|
D6BED1722126661300F02DA0 /* Views */,
|
||||||
|
@ -1422,6 +1428,14 @@
|
||||||
path = OpenInTusker;
|
path = OpenInTusker;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
D6E9CDA6281A426700BBC98E /* Services */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
D6E9CDA7281A427800BBC98E /* PostService.swift */,
|
||||||
|
);
|
||||||
|
path = Services;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
D6F1F84E2193B9BE00F5FE67 /* Caching */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
@ -1827,6 +1841,7 @@
|
||||||
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
D64AAE9726C88DC400FC57FB /* ToastConfiguration.swift in Sources */,
|
||||||
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
D6093FB025BE0B01004811E6 /* TrendingHashtagTableViewCell.swift in Sources */,
|
||||||
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
D63661C02381C144004B9E16 /* PreferencesNavigationController.swift in Sources */,
|
||||||
|
D6E9CDA8281A427800BBC98E /* PostService.swift in Sources */,
|
||||||
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
D6B053A223BD2C0600A066FA /* AssetPickerViewController.swift in Sources */,
|
||||||
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
D6EE63FB2551F7F60065485C /* StatusCollapseButton.swift in Sources */,
|
||||||
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
D627944A23A6AD6100D38C68 /* BookmarksTableViewController.swift in Sources */,
|
||||||
|
@ -1843,6 +1858,7 @@
|
||||||
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
D64AAE9526C88C5000FC57FB /* ToastableViewController.swift in Sources */,
|
||||||
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
D6420AEE26BED18B00ED8175 /* PublicTimelineDescriptionTableViewCell.swift in Sources */,
|
||||||
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
D6E0DC8E216EDF1E00369478 /* Previewing.swift in Sources */,
|
||||||
|
D6B93667281D937300237D0E /* MainSidebarMyProfileCollectionViewCell.swift in Sources */,
|
||||||
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
D6BED174212667E900F02DA0 /* TimelineStatusTableViewCell.swift in Sources */,
|
||||||
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
D69693F42585941A00F4E116 /* UIWindowSceneDelegate+Close.swift in Sources */,
|
||||||
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
D6C143DA253510F4007DC240 /* ComposeEmojiTextField.swift in Sources */,
|
||||||
|
|
|
@ -24,6 +24,19 @@ extension Status.Visibility {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var subtitle: String {
|
||||||
|
switch self {
|
||||||
|
case .public:
|
||||||
|
return "Everyone"
|
||||||
|
case .unlisted:
|
||||||
|
return "Hidden from public timelines"
|
||||||
|
case .private:
|
||||||
|
return "Followers only"
|
||||||
|
case .direct:
|
||||||
|
return "Mentioned users only"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var imageName: String {
|
var imageName: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .public:
|
case .public:
|
||||||
|
|
|
@ -17,6 +17,10 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
|
|
||||||
private var launchActivity: NSUserActivity?
|
private var launchActivity: NSUserActivity?
|
||||||
|
|
||||||
|
var rootViewController: TuskerRootViewController? {
|
||||||
|
window?.rootViewController as? TuskerRootViewController
|
||||||
|
}
|
||||||
|
|
||||||
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
|
||||||
guard let windowScene = scene as? UIWindowScene else { return }
|
guard let windowScene = scene as? UIWindowScene else { return }
|
||||||
|
|
||||||
|
@ -56,7 +60,7 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
if url.host == "x-callback-url" {
|
if url.host == "x-callback-url" {
|
||||||
_ = XCBManager.handle(url: url)
|
_ = XCBManager.handle(url: url)
|
||||||
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
} else if var components = URLComponents(url: url, resolvingAgainstBaseURL: false),
|
||||||
let rootViewController = window!.rootViewController as? TuskerRootViewController {
|
let rootViewController = rootViewController {
|
||||||
components.scheme = "https"
|
components.scheme = "https"
|
||||||
let query = components.string!
|
let query = components.string!
|
||||||
rootViewController.performSearch(query: query)
|
rootViewController.performSearch(query: query)
|
||||||
|
@ -202,6 +206,12 @@ class MainSceneDelegate: UIResponder, UIWindowSceneDelegate {
|
||||||
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
window?.overrideUserInterfaceStyle = Preferences.shared.theme
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func showAddAccount() {
|
||||||
|
rootViewController?.presentPreferences {
|
||||||
|
NotificationCenter.default.post(name: .addAccount, object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
extension MainSceneDelegate: OnboardingViewControllerDelegate {
|
||||||
|
|
|
@ -88,8 +88,13 @@ enum CompositionAttachmentData {
|
||||||
options.isNetworkAccessAllowed = true
|
options.isNetworkAccessAllowed = true
|
||||||
options.version = .current
|
options.version = .current
|
||||||
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
PHImageManager.default().requestExportSession(forVideo: asset, options: options, exportPreset: AVAssetExportPresetHighestQuality) { (exportSession, info) in
|
||||||
guard let exportSession = exportSession else { fatalError("failed to create export session") }
|
if let exportSession = exportSession {
|
||||||
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
CompositionAttachmentData.exportVideoData(session: exportSession, completion: completion)
|
||||||
|
} else if let error = info?[PHImageErrorKey] as? Error {
|
||||||
|
completion(.failure(.videoExport(error)))
|
||||||
|
} else {
|
||||||
|
completion(.failure(.noVideoExportSession))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
fatalError("assetType must be either image or video")
|
fatalError("assetType must be either image or video")
|
||||||
|
@ -97,7 +102,8 @@ enum CompositionAttachmentData {
|
||||||
case let .video(url):
|
case let .video(url):
|
||||||
let asset = AVURLAsset(url: url)
|
let asset = AVURLAsset(url: url)
|
||||||
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
guard let session = AVAssetExportSession(asset: asset, presetName: AVAssetExportPresetHighestQuality) else {
|
||||||
fatalError("failed to create export session")
|
completion(.failure(.noVideoExportSession))
|
||||||
|
return
|
||||||
}
|
}
|
||||||
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
CompositionAttachmentData.exportVideoData(session: session, completion: completion)
|
||||||
|
|
||||||
|
@ -112,14 +118,14 @@ enum CompositionAttachmentData {
|
||||||
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
session.outputURL = FileManager.default.temporaryDirectory.appendingPathComponent("exported_video_\(UUID())").appendingPathExtension("mp4")
|
||||||
session.exportAsynchronously {
|
session.exportAsynchronously {
|
||||||
guard session.status == .completed else {
|
guard session.status == .completed else {
|
||||||
completion(.failure(.export(session.error!)))
|
completion(.failure(.videoExport(session.error!)))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
do {
|
do {
|
||||||
let data = try Data(contentsOf: session.outputURL!)
|
let data = try Data(contentsOf: session.outputURL!)
|
||||||
completion(.success((data, "video/mp4")))
|
completion(.success((data, "video/mp4")))
|
||||||
} catch {
|
} catch {
|
||||||
completion(.failure(.export(error)))
|
completion(.failure(.videoExport(error)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -128,9 +134,21 @@ enum CompositionAttachmentData {
|
||||||
case image, video
|
case image, video
|
||||||
}
|
}
|
||||||
|
|
||||||
enum Error: Swift.Error {
|
enum Error: Swift.Error, LocalizedError {
|
||||||
case missingData
|
case missingData
|
||||||
case export(Swift.Error)
|
case videoExport(Swift.Error)
|
||||||
|
case noVideoExportSession
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case .missingData:
|
||||||
|
return "Missing Data"
|
||||||
|
case .videoExport(let error):
|
||||||
|
return "Exporting video: \(error)"
|
||||||
|
case .noVideoExportSession:
|
||||||
|
return "Couldn't create video export session"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -250,7 +250,7 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
item.accessibilityLabel = String(format: NSLocalizedString("Visibility: %@", comment: "compose visiblity accessibility label"), draft.visibility.displayName)
|
||||||
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
let elements = Status.Visibility.allCases.map { (visibility) -> UIMenuElement in
|
||||||
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
|
let state = visibility == newVisibility ? UIMenuElement.State.on : .off
|
||||||
return UIAction(title: visibility.displayName, image: UIImage(systemName: visibility.unfilledImageName), identifier: nil, discoverabilityTitle: nil, attributes: [], state: state) { [unowned self] (_) in
|
return UIAction(title: visibility.displayName, subtitle: visibility.subtitle, image: UIImage(systemName: visibility.unfilledImageName), state: state) { [unowned self] (_) in
|
||||||
self.draft.visibility = visibility
|
self.draft.visibility = visibility
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -270,9 +270,9 @@ class ComposeHostingController: UIHostingController<ComposeContainerView> {
|
||||||
item.image = UIImage(systemName: "link")
|
item.image = UIImage(systemName: "link")
|
||||||
item.accessibilityLabel = "Federated"
|
item.accessibilityLabel = "Federated"
|
||||||
}
|
}
|
||||||
|
let instanceSubtitle = "Only \(mastodonController.accountInfo!.instanceURL.host!)"
|
||||||
item.menu = UIMenu(children: [
|
item.menu = UIMenu(children: [
|
||||||
// todo: iOS 15, action subtitles
|
UIAction(title: "Local-only", subtitle: instanceSubtitle, image: UIImage(named: "link.broken"), state: localOnly ? .on : .off) { [unowned self] (_) in
|
||||||
UIAction(title: "Local-only", image: UIImage(named: "link.broken"), state: localOnly ? .on : .off) { [unowned self] (_) in
|
|
||||||
self.draft.localOnly = true
|
self.draft.localOnly = true
|
||||||
},
|
},
|
||||||
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: localOnly ? .off : .on) { [unowned self] (_) in
|
UIAction(title: "Federated", image: UIImage(systemName: "link"), state: localOnly ? .off : .on) { [unowned self] (_) in
|
||||||
|
@ -474,3 +474,13 @@ extension ComposeHostingController: ComposeDrawingViewControllerDelegate {
|
||||||
dismiss(animated: true)
|
dismiss(animated: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fileprivate extension UIAction {
|
||||||
|
convenience init(title: String, subtitle: String?, image: UIImage?, state: UIAction.State, handler: @escaping UIActionHandler) {
|
||||||
|
if #available(iOS 15.0, *) {
|
||||||
|
self.init(title: title, subtitle: subtitle, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||||
|
} else {
|
||||||
|
self.init(title: title, image: image, identifier: nil, discoverabilityTitle: nil, attributes: [], state: state, handler: handler)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -10,16 +10,49 @@ import SwiftUI
|
||||||
import Pachyderm
|
import Pachyderm
|
||||||
import Combine
|
import Combine
|
||||||
|
|
||||||
|
@propertyWrapper struct OptionalStateObject<T: ObservableObject>: DynamicProperty {
|
||||||
|
private class Republisher: ObservableObject {
|
||||||
|
var cancellable: AnyCancellable?
|
||||||
|
var wrapped: T? {
|
||||||
|
didSet {
|
||||||
|
cancellable?.cancel()
|
||||||
|
cancellable = wrapped?.objectWillChange
|
||||||
|
.receive(on: RunLoop.main)
|
||||||
|
.sink { [unowned self] _ in
|
||||||
|
self.objectWillChange.send()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@StateObject private var republisher = Republisher()
|
||||||
|
@State private var object: T?
|
||||||
|
var wrappedValue: T? {
|
||||||
|
get {
|
||||||
|
object
|
||||||
|
}
|
||||||
|
nonmutating set {
|
||||||
|
object = newValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func update() {
|
||||||
|
republisher.wrapped = wrappedValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct ComposeView: View {
|
struct ComposeView: View {
|
||||||
@ObservedObject var draft: Draft
|
@ObservedObject var draft: Draft
|
||||||
@EnvironmentObject var mastodonController: MastodonController
|
@EnvironmentObject var mastodonController: MastodonController
|
||||||
@EnvironmentObject var uiState: ComposeUIState
|
@EnvironmentObject var uiState: ComposeUIState
|
||||||
|
|
||||||
@State private var isPosting = false
|
@OptionalStateObject private var poster: PostService?
|
||||||
@State private var postProgress: Double = 0
|
|
||||||
@State private var postTotalProgress: Double = 0
|
|
||||||
@State private var isShowingPostErrorAlert = false
|
@State private var isShowingPostErrorAlert = false
|
||||||
@State private var postError: PostError?
|
@State private var postError: PostService.Error?
|
||||||
|
|
||||||
|
private var isPosting: Bool {
|
||||||
|
poster != nil
|
||||||
|
}
|
||||||
|
|
||||||
private let stackPadding: CGFloat = 8
|
private let stackPadding: CGFloat = 8
|
||||||
|
|
||||||
|
@ -58,9 +91,9 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if postProgress > 0 {
|
if let poster = poster {
|
||||||
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
// can't use SwiftUI.ProgressView because there's no UIProgressView.Style.bar equivalent, see FB8587149
|
||||||
WrappedProgressView(value: postProgress, total: postTotalProgress)
|
WrappedProgressView(value: poster.currentStep, total: poster.totalSteps)
|
||||||
}
|
}
|
||||||
|
|
||||||
autocompleteSuggestions
|
autocompleteSuggestions
|
||||||
|
@ -123,6 +156,7 @@ struct ComposeView: View {
|
||||||
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
// the list rows provide their own padding, so we cancel out the extra spacing from the VStack
|
||||||
.padding([.top, .bottom], -8)
|
.padding([.top, .bottom], -8)
|
||||||
}
|
}
|
||||||
|
.disabled(isPosting)
|
||||||
.padding(stackPadding)
|
.padding(stackPadding)
|
||||||
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
.padding(.bottom, uiState.autocompleteState != nil ? 46 : nil)
|
||||||
}
|
}
|
||||||
|
@ -147,7 +181,11 @@ struct ComposeView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
private var postButton: some View {
|
private var postButton: some View {
|
||||||
Button(action: self.postStatus) {
|
Button {
|
||||||
|
Task {
|
||||||
|
await self.postStatus()
|
||||||
|
}
|
||||||
|
} label: {
|
||||||
Text("Post")
|
Text("Post")
|
||||||
}
|
}
|
||||||
.disabled(!postButtonEnabled)
|
.disabled(!postButtonEnabled)
|
||||||
|
@ -184,177 +222,31 @@ struct ComposeView: View {
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
private func postStatus() {
|
private func postStatus() async {
|
||||||
guard draft.hasContent else { return }
|
guard !isPosting,
|
||||||
|
draft.hasContent else {
|
||||||
isPosting = true
|
|
||||||
|
|
||||||
// save before posting, so if a crash occurs during network request, the status won't be lost
|
|
||||||
DraftsManager.save()
|
|
||||||
|
|
||||||
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
|
||||||
let sensitive = contentWarning != nil
|
|
||||||
|
|
||||||
// 2 steps (request data, then upload) for each attachment
|
|
||||||
postTotalProgress = Double(2 + (draft.attachments.count * 2))
|
|
||||||
postProgress = 1
|
|
||||||
|
|
||||||
uploadAttachments { (result) in
|
|
||||||
switch result {
|
|
||||||
case let .failure(error):
|
|
||||||
self.isShowingPostErrorAlert = true
|
|
||||||
self.postError = error
|
|
||||||
self.postProgress = 0
|
|
||||||
self.postTotalProgress = 0
|
|
||||||
self.isPosting = false
|
|
||||||
|
|
||||||
case let .success(uploadedAttachments):
|
|
||||||
let request = Client.createStatus(text: draft.textForPosting(on: mastodonController.instanceFeatures),
|
|
||||||
contentType: Preferences.shared.statusContentType,
|
|
||||||
inReplyTo: draft.inReplyToID,
|
|
||||||
media: uploadedAttachments,
|
|
||||||
sensitive: sensitive,
|
|
||||||
spoilerText: contentWarning,
|
|
||||||
visibility: draft.visibility,
|
|
||||||
language: nil,
|
|
||||||
pollOptions: draft.poll?.options.map(\.text),
|
|
||||||
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
|
||||||
pollMultiple: draft.poll?.multiple,
|
|
||||||
localOnly: mastodonController.instanceFeatures.instanceType == .hometown ? draft.localOnly : nil)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
switch response {
|
|
||||||
case let .failure(error):
|
|
||||||
self.isShowingPostErrorAlert = true
|
|
||||||
self.postError = error
|
|
||||||
|
|
||||||
case .success(_, _):
|
|
||||||
self.postProgress += 1
|
|
||||||
|
|
||||||
DraftsManager.shared.remove(self.draft)
|
|
||||||
|
|
||||||
// wait .25 seconds so the user can see the progress bar has completed
|
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + .milliseconds(250)) {
|
|
||||||
self.uiState.delegate?.dismissCompose(mode: .post)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func uploadAttachments(_ completion: @escaping (Result<[Attachment], AttachmentUploadError>) -> Void) {
|
|
||||||
let group = DispatchGroup()
|
|
||||||
|
|
||||||
var attachmentDataResults = [Result<(Data, String), CompositionAttachmentData.Error>?]()
|
|
||||||
|
|
||||||
for (index, compAttachment) in draft.attachments.enumerated() {
|
|
||||||
group.enter()
|
|
||||||
|
|
||||||
attachmentDataResults.append(nil)
|
|
||||||
|
|
||||||
compAttachment.data.getData { (result) in
|
|
||||||
postProgress += 1
|
|
||||||
|
|
||||||
attachmentDataResults[index] = result
|
|
||||||
group.leave()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
group.notify(queue: .global(qos: .userInitiated)) {
|
|
||||||
|
|
||||||
var anyFailed = false
|
|
||||||
var uploadedAttachments = [Result<Attachment, Error>?]()
|
|
||||||
|
|
||||||
// Mastodon does not respect the order of the `media_ids` parameter in the create post request,
|
|
||||||
// it determines attachment order by which was uploaded first. Since the upload attachment request
|
|
||||||
// does not include any timestamp data, and requests may arrive at the server out-of-order,
|
|
||||||
// attachments need to be uploaded serially in order to ensure the order of attachments in the
|
|
||||||
// posted status reflects order the user set.
|
|
||||||
// Pleroma does respect the order of the `media_ids` parameter.
|
|
||||||
|
|
||||||
let datas: [(Data, String)]
|
|
||||||
do {
|
|
||||||
datas = try attachmentDataResults.map { try $0!.get() }
|
|
||||||
} catch {
|
|
||||||
completion(.failure(AttachmentUploadError(errors: [error])))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
for (index, (data, mimeType)) in datas.enumerated() {
|
let poster = PostService(mastodonController: mastodonController, draft: draft)
|
||||||
group.enter()
|
self.poster = poster
|
||||||
|
|
||||||
let compAttachment = draft.attachments[index]
|
do {
|
||||||
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
try await poster.post()
|
||||||
let request = Client.upload(attachment: formAttachment, description: compAttachment.attachmentDescription)
|
|
||||||
self.mastodonController.run(request) { (response) in
|
|
||||||
switch response {
|
|
||||||
case let .failure(error):
|
|
||||||
uploadedAttachments.append(.failure(error))
|
|
||||||
anyFailed = true
|
|
||||||
|
|
||||||
case let .success(attachment, _):
|
// wait .25 seconds so the user can see the progress bar has completed
|
||||||
self.postProgress += 1
|
try? await Task.sleep(nanoseconds: 250_000_000)
|
||||||
uploadedAttachments.append(.success(attachment))
|
|
||||||
|
uiState.delegate?.dismissCompose(mode: .post)
|
||||||
|
|
||||||
|
} catch let error as PostService.Error {
|
||||||
|
self.isShowingPostErrorAlert = true
|
||||||
|
self.postError = error
|
||||||
|
} catch {
|
||||||
|
fatalError("Unreachable")
|
||||||
}
|
}
|
||||||
|
|
||||||
group.leave()
|
self.poster = nil
|
||||||
}
|
|
||||||
|
|
||||||
group.wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if anyFailed {
|
|
||||||
let errors = uploadedAttachments.map { (result) -> Error? in
|
|
||||||
if case let .failure(error) = result {
|
|
||||||
return error
|
|
||||||
} else {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
completion(.failure(AttachmentUploadError(errors: errors)))
|
|
||||||
} else {
|
|
||||||
let uploadedAttachments = uploadedAttachments.map {
|
|
||||||
try! $0!.get()
|
|
||||||
}
|
|
||||||
completion(.success(uploadedAttachments))
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fileprivate protocol PostError: LocalizedError {}
|
|
||||||
|
|
||||||
extension PostError {
|
|
||||||
var localizedDescription: String {
|
|
||||||
if let self = self as? Client.Error {
|
|
||||||
return self.localizedDescription
|
|
||||||
} else if let self = self as? AttachmentUploadError {
|
|
||||||
return self.localizedDescription
|
|
||||||
} else {
|
|
||||||
return "Unknown Error"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extension Client.Error: PostError {}
|
|
||||||
|
|
||||||
fileprivate struct AttachmentUploadError: PostError {
|
|
||||||
let errors: [Error?]
|
|
||||||
|
|
||||||
var localizedDescription: String {
|
|
||||||
return errors.enumerated().compactMap { (index, error) -> String? in
|
|
||||||
guard let error = error else { return nil }
|
|
||||||
let description: String
|
|
||||||
// need to downcast to use more specific localizedDescription impl from Pachyderm
|
|
||||||
if let error = error as? Client.Error {
|
|
||||||
description = error.localizedDescription
|
|
||||||
} else {
|
|
||||||
description = error.localizedDescription
|
|
||||||
}
|
|
||||||
return "Attachment \(index + 1): \(description)"
|
|
||||||
}.joined(separator: ",\n")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -9,6 +9,8 @@
|
||||||
import UIKit
|
import UIKit
|
||||||
|
|
||||||
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
protocol FastAccountSwitcherViewControllerDelegate: AnyObject {
|
||||||
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController)
|
||||||
|
/// - Parameter point: In the coordinate space of the view to which the pan gesture recognizer is attached.
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -20,11 +22,13 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
@IBOutlet weak var blurContentView: UIView!
|
@IBOutlet weak var blurContentView: UIView!
|
||||||
@IBOutlet weak var accountsStack: UIStackView!
|
@IBOutlet weak var accountsStack: UIStackView!
|
||||||
|
|
||||||
private var accountViews: [FastSwitchingAccountView] = []
|
private(set) var accountViews: [FastSwitchingAccountView] = []
|
||||||
private var lastSelectedAccountViewIndex: Int?
|
private var lastSelectedAccountViewIndex: Int?
|
||||||
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
private var selectionChangedFeedbackGenerator: UIImpactFeedbackGenerator?
|
||||||
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
private var touchBeganFeedbackWorkItem: DispatchWorkItem?
|
||||||
|
|
||||||
|
var itemOrientation: ItemOrientation = .iconsTrailing
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
super.init(nibName: "FastAccountSwitcherViewController", bundle: .main)
|
||||||
}
|
}
|
||||||
|
@ -51,6 +55,15 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
func show() {
|
func show() {
|
||||||
createAccountViews()
|
createAccountViews()
|
||||||
|
// add after creating account views so that the presenter can align based on them
|
||||||
|
delegate?.fastAccountSwitcherAddToViewHierarchy(self)
|
||||||
|
|
||||||
|
switch itemOrientation {
|
||||||
|
case .iconsLeading:
|
||||||
|
accountsStack.alignment = .leading
|
||||||
|
case .iconsTrailing:
|
||||||
|
accountsStack.alignment = .trailing
|
||||||
|
}
|
||||||
|
|
||||||
view.isHidden = false
|
view.isHidden = false
|
||||||
|
|
||||||
|
@ -87,24 +100,35 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func hide(completion: (() -> Void)? = nil) {
|
func hide(completion: (() -> Void)? = nil) {
|
||||||
|
guard view.superview != nil else {
|
||||||
|
return
|
||||||
|
}
|
||||||
lastSelectedAccountViewIndex = nil
|
lastSelectedAccountViewIndex = nil
|
||||||
selectionChangedFeedbackGenerator = nil
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
|
UIView.animate(withDuration: 0.15, delay: 0, options: .curveEaseInOut) {
|
||||||
self.view.alpha = 0
|
self.view.alpha = 0
|
||||||
} completion: { (_) in
|
} completion: { (_) in
|
||||||
|
// todo: probably remove these two lines
|
||||||
self.view.alpha = 1
|
self.view.alpha = 1
|
||||||
self.view.isHidden = true
|
self.view.isHidden = true
|
||||||
completion?()
|
completion?()
|
||||||
|
self.view.removeFromSuperview()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func createAccountViews() {
|
private func createAccountViews() {
|
||||||
accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
accountsStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||||
accountViews = []
|
|
||||||
|
let addAccountPlaceholder = FastSwitchingAccountView(orientation: itemOrientation)
|
||||||
|
accountsStack.addArrangedSubview(addAccountPlaceholder)
|
||||||
|
|
||||||
|
accountViews = [
|
||||||
|
addAccountPlaceholder
|
||||||
|
]
|
||||||
|
|
||||||
for account in LocalData.shared.accounts {
|
for account in LocalData.shared.accounts {
|
||||||
let accountView = FastSwitchingAccountView(account: account)
|
let accountView = FastSwitchingAccountView(account: account, orientation: itemOrientation)
|
||||||
accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID
|
accountView.isCurrent = account.id == LocalData.shared.mostRecentAccountID
|
||||||
accountsStack.addArrangedSubview(accountView)
|
accountsStack.addArrangedSubview(accountView)
|
||||||
accountViews.append(accountView)
|
accountViews.append(accountView)
|
||||||
|
@ -122,7 +146,17 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
private func switchAccount(newIndex: Int, hapticFeedback: Bool = true) {
|
||||||
let account = LocalData.shared.accounts[newIndex]
|
if newIndex == 0 { // add account placeholder
|
||||||
|
if hapticFeedback {
|
||||||
|
selectionChangedFeedbackGenerator?.impactOccurred()
|
||||||
|
}
|
||||||
|
selectionChangedFeedbackGenerator = nil
|
||||||
|
|
||||||
|
hide() {
|
||||||
|
(self.view.window!.windowScene!.delegate as! MainSceneDelegate).showAddAccount()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let account = LocalData.shared.accounts[newIndex - 1]
|
||||||
|
|
||||||
if account.id != LocalData.shared.mostRecentAccountID {
|
if account.id != LocalData.shared.mostRecentAccountID {
|
||||||
if hapticFeedback {
|
if hapticFeedback {
|
||||||
|
@ -137,6 +171,7 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// MARK: - Interaction
|
// MARK: - Interaction
|
||||||
|
|
||||||
|
@ -155,10 +190,9 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
handleGestureMoved(to: location)
|
handleGestureMoved(to: location)
|
||||||
|
|
||||||
case .ended:
|
case .ended:
|
||||||
let location = recognizer.location(in: view)
|
|
||||||
if let index = lastSelectedAccountViewIndex {
|
if let index = lastSelectedAccountViewIndex {
|
||||||
switchAccount(newIndex: index)
|
switchAccount(newIndex: index)
|
||||||
} else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: location) ?? false) {
|
} else if !(delegate?.fastAccountSwitcher(self, triggerZoneContains: recognizer.location(in: recognizer.view)) ?? false) {
|
||||||
hide()
|
hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -240,9 +274,16 @@ class FastAccountSwitcherViewController: UIViewController {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension FastAccountSwitcherViewController {
|
||||||
|
enum ItemOrientation {
|
||||||
|
case iconsLeading
|
||||||
|
case iconsTrailing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate {
|
extension FastAccountSwitcherViewController: UIGestureRecognizerDelegate {
|
||||||
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
func gestureRecognizerShouldBegin(_ gestureRecognizer: UIGestureRecognizer) -> Bool {
|
||||||
let point = gestureRecognizer.location(in: view)
|
let point = gestureRecognizer.location(in: gestureRecognizer.view)
|
||||||
return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false
|
return delegate?.fastAccountSwitcher(self, triggerZoneContains: point) ?? false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="17506" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
<document type="com.apple.InterfaceBuilder3.CocoaTouch.XIB" version="3.0" toolsVersion="20037" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES">
|
||||||
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
<device id="retina6_1" orientation="portrait" appearance="light"/>
|
||||||
<dependencies>
|
<dependencies>
|
||||||
<deployment identifier="iOS"/>
|
<deployment identifier="iOS"/>
|
||||||
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="17504.1"/>
|
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="20020"/>
|
||||||
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
|
||||||
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
@ -12,7 +12,6 @@
|
||||||
<connections>
|
<connections>
|
||||||
<outlet property="accountsStack" destination="lYU-Bb-3Wi" id="Dxs-ta-ORu"/>
|
<outlet property="accountsStack" destination="lYU-Bb-3Wi" id="Dxs-ta-ORu"/>
|
||||||
<outlet property="blurContentView" destination="1Gd-Da-Vab" id="JqT-uq-1o2"/>
|
<outlet property="blurContentView" destination="1Gd-Da-Vab" id="JqT-uq-1o2"/>
|
||||||
<outlet property="dimmingView" destination="Lul-oI-bZ7" id="JhP-ZX-8fb"/>
|
|
||||||
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
<outlet property="view" destination="i5M-Pr-FkT" id="sfx-zR-JGt"/>
|
||||||
</connections>
|
</connections>
|
||||||
</placeholder>
|
</placeholder>
|
||||||
|
@ -21,10 +20,6 @@
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
|
||||||
<subviews>
|
<subviews>
|
||||||
<view alpha="0.25" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="Lul-oI-bZ7">
|
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
|
||||||
<color key="backgroundColor" white="0.0" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
|
|
||||||
</view>
|
|
||||||
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5fd-Ni-Owc">
|
<visualEffectView opaque="NO" contentMode="scaleToFill" translatesAutoresizingMaskIntoConstraints="NO" id="5fd-Ni-Owc">
|
||||||
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
<rect key="frame" x="0.0" y="0.0" width="414" height="896"/>
|
||||||
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="1Gd-Da-Vab">
|
<view key="contentView" opaque="NO" clipsSubviews="YES" multipleTouchEnabled="YES" contentMode="center" insetsLayoutMarginsFromSafeArea="NO" id="1Gd-Da-Vab">
|
||||||
|
@ -38,7 +33,7 @@
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="top" secondItem="1Gd-Da-Vab" secondAttribute="topMargin" placeholder="YES" id="KQs-d5-U3f"/>
|
||||||
<constraint firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailingMargin" constant="8" id="UZh-xR-XVt"/>
|
<constraint firstAttribute="trailing" secondItem="lYU-Bb-3Wi" secondAttribute="trailingMargin" constant="8" id="UZh-xR-XVt"/>
|
||||||
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" id="j6f-r5-NNI"/>
|
<constraint firstAttribute="bottomMargin" secondItem="lYU-Bb-3Wi" secondAttribute="bottom" placeholder="YES" id="j6f-r5-NNI"/>
|
||||||
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
<constraint firstItem="lYU-Bb-3Wi" firstAttribute="leading" secondItem="1Gd-Da-Vab" secondAttribute="leading" constant="8" id="sae-ga-MGE"/>
|
||||||
</constraints>
|
</constraints>
|
||||||
</view>
|
</view>
|
||||||
|
@ -48,20 +43,11 @@
|
||||||
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
<viewLayoutGuide key="safeArea" id="fnl-2z-Ty3"/>
|
||||||
<gestureRecognizers/>
|
<gestureRecognizers/>
|
||||||
<constraints>
|
<constraints>
|
||||||
<constraint firstAttribute="trailing" secondItem="Lul-oI-bZ7" secondAttribute="trailing" id="9Fp-IG-O9W"/>
|
|
||||||
<constraint firstAttribute="trailing" secondItem="5fd-Ni-Owc" secondAttribute="trailing" id="c27-P9-lLK"/>
|
<constraint firstAttribute="trailing" secondItem="5fd-Ni-Owc" secondAttribute="trailing" id="c27-P9-lLK"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="Lul-oI-bZ7" secondAttribute="bottom" id="o6y-tG-MwH"/>
|
|
||||||
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="phf-PC-bdH"/>
|
||||||
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
<constraint firstItem="5fd-Ni-Owc" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="rz7-cQ-PIC"/>
|
||||||
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
<constraint firstAttribute="bottom" secondItem="5fd-Ni-Owc" secondAttribute="bottom" id="sHl-iD-kGi"/>
|
||||||
<constraint firstItem="Lul-oI-bZ7" firstAttribute="leading" secondItem="i5M-Pr-FkT" secondAttribute="leading" id="tfE-Xr-YBo"/>
|
|
||||||
<constraint firstItem="Lul-oI-bZ7" firstAttribute="top" secondItem="i5M-Pr-FkT" secondAttribute="top" id="ua7-DO-kdp"/>
|
|
||||||
</constraints>
|
</constraints>
|
||||||
<variation key="default">
|
|
||||||
<mask key="subviews">
|
|
||||||
<exclude reference="Lul-oI-bZ7"/>
|
|
||||||
</mask>
|
|
||||||
</variation>
|
|
||||||
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
<point key="canvasLocation" x="140.57971014492756" y="144.64285714285714"/>
|
||||||
</view>
|
</view>
|
||||||
</objects>
|
</objects>
|
||||||
|
|
|
@ -10,8 +10,6 @@ import UIKit
|
||||||
|
|
||||||
class FastSwitchingAccountView: UIView {
|
class FastSwitchingAccountView: UIView {
|
||||||
|
|
||||||
let account: LocalData.UserAccountInfo
|
|
||||||
|
|
||||||
private static let selectedColor = UIColor { (traits) in
|
private static let selectedColor = UIColor { (traits) in
|
||||||
if traits.userInterfaceStyle == .dark {
|
if traits.userInterfaceStyle == .dark {
|
||||||
return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 100 / 100, alpha: 1)
|
return UIColor(hue: 211 / 360, saturation: 85 / 100, brightness: 100 / 100, alpha: 1)
|
||||||
|
@ -37,23 +35,38 @@ class FastSwitchingAccountView: UIView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private let orientation: FastAccountSwitcherViewController.ItemOrientation
|
||||||
|
|
||||||
private let usernameLabel = UILabel()
|
private let usernameLabel = UILabel()
|
||||||
private let instanceLabel = UILabel()
|
private let instanceLabel = UILabel()
|
||||||
|
private let avatarImageView = UIImageView()
|
||||||
|
|
||||||
private var avatarRequest: ImageCache.Request?
|
private var avatarRequest: ImageCache.Request?
|
||||||
|
|
||||||
init(account: LocalData.UserAccountInfo) {
|
init(account: LocalData.UserAccountInfo, orientation: FastAccountSwitcherViewController.ItemOrientation) {
|
||||||
self.account = account
|
self.orientation = orientation
|
||||||
|
|
||||||
super.init(frame: .zero)
|
super.init(frame: .zero)
|
||||||
|
commonInit()
|
||||||
|
setupAccount(account: account)
|
||||||
|
}
|
||||||
|
|
||||||
|
init(orientation: FastAccountSwitcherViewController.ItemOrientation) {
|
||||||
|
self.orientation = orientation
|
||||||
|
super.init(frame: .zero)
|
||||||
|
commonInit()
|
||||||
|
setupPlaceholder()
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
private func commonInit() {
|
||||||
usernameLabel.textColor = .white
|
usernameLabel.textColor = .white
|
||||||
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
|
usernameLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .headline), size: 0)
|
||||||
usernameLabel.text = account.username
|
|
||||||
|
|
||||||
instanceLabel.textColor = .white
|
instanceLabel.textColor = .white
|
||||||
instanceLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0)
|
instanceLabel.font = UIFont(descriptor: .preferredFontDescriptor(withTextStyle: .subheadline), size: 0)
|
||||||
instanceLabel.text = account.instanceURL.host!
|
|
||||||
|
|
||||||
let stackView = UIStackView(arrangedSubviews: [
|
let stackView = UIStackView(arrangedSubviews: [
|
||||||
usernameLabel,
|
usernameLabel,
|
||||||
|
@ -61,45 +74,66 @@ class FastSwitchingAccountView: UIView {
|
||||||
])
|
])
|
||||||
stackView.translatesAutoresizingMaskIntoConstraints = false
|
stackView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stackView.axis = .vertical
|
stackView.axis = .vertical
|
||||||
stackView.alignment = .trailing
|
|
||||||
addSubview(stackView)
|
addSubview(stackView)
|
||||||
|
|
||||||
let avatarImageView = UIImageView()
|
|
||||||
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
avatarImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||||
avatarImageView.layer.masksToBounds = true
|
avatarImageView.layer.masksToBounds = true
|
||||||
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 40
|
avatarImageView.layer.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * 40
|
||||||
avatarImageView.image = UIImage(systemName: Preferences.shared.avatarStyle == .circle ? "person.crop.circle" : "person.crop.square")
|
avatarImageView.image = UIImage(systemName: Preferences.shared.avatarStyle == .circle ? "person.crop.circle" : "person.crop.square")
|
||||||
avatarImageView.contentMode = .scaleAspectFit
|
avatarImageView.contentMode = .scaleAspectFill
|
||||||
addSubview(avatarImageView)
|
addSubview(avatarImageView)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
avatarImageView.topAnchor.constraint(equalTo: topAnchor, constant: 8),
|
||||||
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
avatarImageView.bottomAnchor.constraint(equalTo: bottomAnchor, constant: -8),
|
||||||
avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
|
||||||
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
|
avatarImageView.widthAnchor.constraint(equalToConstant: 40),
|
||||||
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor),
|
avatarImageView.heightAnchor.constraint(equalTo: avatarImageView.widthAnchor),
|
||||||
|
|
||||||
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
|
||||||
stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8),
|
|
||||||
stackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
stackView.centerYAnchor.constraint(equalTo: avatarImageView.centerYAnchor),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
switch orientation {
|
||||||
|
case .iconsLeading:
|
||||||
|
stackView.alignment = .leading
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: avatarImageView.trailingAnchor, constant: 8),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
])
|
||||||
|
|
||||||
|
case .iconsTrailing:
|
||||||
|
stackView.alignment = .trailing
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
avatarImageView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||||
|
stackView.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 8),
|
||||||
|
stackView.trailingAnchor.constraint(equalTo: avatarImageView.leadingAnchor, constant: -8),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLabelColors()
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupAccount(account: LocalData.UserAccountInfo) {
|
||||||
|
usernameLabel.text = account.username
|
||||||
|
instanceLabel.text = account.instanceURL.host!
|
||||||
let controller = MastodonController.getForAccount(account)
|
let controller = MastodonController.getForAccount(account)
|
||||||
controller.getOwnAccount { [weak self] (result) in
|
controller.getOwnAccount { [weak self] (result) in
|
||||||
guard let self = self,
|
guard let self = self,
|
||||||
case let .success(account) = result,
|
case let .success(account) = result,
|
||||||
let avatar = account.avatar else { return }
|
let avatar = account.avatar else { return }
|
||||||
self.avatarRequest = ImageCache.avatars.get(avatar) { [weak avatarImageView] (_, image) in
|
self.avatarRequest = ImageCache.avatars.get(avatar) { [weak self] (_, image) in
|
||||||
guard let avatarImageView = avatarImageView, let image = image else { return }
|
guard let self = self, let image = image else { return }
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
avatarImageView.image = image
|
self.avatarImageView.image = image
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
required init?(coder: NSCoder) {
|
private func setupPlaceholder() {
|
||||||
fatalError("init(coder:) has not been implemented")
|
usernameLabel.text = "Add Account"
|
||||||
|
instanceLabel.isHidden = true
|
||||||
|
avatarImageView.image = UIImage(systemName: "plus")
|
||||||
}
|
}
|
||||||
|
|
||||||
private func updateLabelColors() {
|
private func updateLabelColors() {
|
||||||
|
@ -113,6 +147,7 @@ class FastSwitchingAccountView: UIView {
|
||||||
}
|
}
|
||||||
usernameLabel.textColor = color
|
usernameLabel.textColor = color
|
||||||
instanceLabel.textColor = color
|
instanceLabel.textColor = color
|
||||||
|
avatarImageView.tintColor = color
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -86,8 +86,14 @@ extension AccountSwitchingContainerViewController: TuskerRootViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
func performSearch(query: String) {
|
func performSearch(query: String) {
|
||||||
|
loadViewIfNeeded()
|
||||||
root.performSearch(query: query)
|
root.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentPreferences(completion: (() -> Void)?) {
|
||||||
|
loadViewIfNeeded()
|
||||||
|
root.presentPreferences(completion: completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
extension AccountSwitchingContainerViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -0,0 +1,86 @@
|
||||||
|
//
|
||||||
|
// MainSidebarMyProfileCollectionViewCell.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/30/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import UIKit
|
||||||
|
|
||||||
|
class MainSidebarMyProfileCollectionViewCell: UICollectionViewListCell {
|
||||||
|
|
||||||
|
private var verticalImageInset: CGFloat {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
return (28 - avatarImageSize) / 2
|
||||||
|
} else {
|
||||||
|
return (44 - avatarImageSize) / 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
private var avatarImageSize: CGFloat {
|
||||||
|
if UIDevice.current.userInterfaceIdiom == .mac {
|
||||||
|
return 20
|
||||||
|
} else {
|
||||||
|
return 28
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override init(frame: CGRect) {
|
||||||
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(self, selector: #selector(preferencesChanged), name: .preferencesChanged, object: nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
required init?(coder: NSCoder) {
|
||||||
|
fatalError("init(coder:) has not been implemented")
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateUI(item: MainSidebarViewController.Item, account: LocalData.UserAccountInfo) async {
|
||||||
|
var config = defaultContentConfiguration()
|
||||||
|
config.text = item.title
|
||||||
|
config.image = UIImage(systemName: item.imageName!)
|
||||||
|
self.contentConfiguration = config
|
||||||
|
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||||
|
let indicator = FastAccountSwitcherIndicatorView()
|
||||||
|
// need to explicitly set the frame to get it vertically centered
|
||||||
|
indicator.frame = CGRect(origin: .zero, size: indicator.intrinsicContentSize)
|
||||||
|
accessories = [
|
||||||
|
.customView(configuration: .init(customView: indicator, placement: .trailing()))
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
let mastodonController = MastodonController.getForAccount(account)
|
||||||
|
guard let account = try? await mastodonController.getOwnAccount(),
|
||||||
|
let avatar = account.avatar else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = ImageCache.avatars.get(avatar, loadOriginal: false) { [weak self] _, image in
|
||||||
|
guard let self = self,
|
||||||
|
let image = image else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
guard var config = self.contentConfiguration as? UIListContentConfiguration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.image = image
|
||||||
|
config.directionalLayoutMargins.top = self.verticalImageInset
|
||||||
|
config.directionalLayoutMargins.bottom = self.verticalImageInset
|
||||||
|
config.imageProperties.maximumSize = CGSize(width: self.avatarImageSize, height: self.avatarImageSize)
|
||||||
|
config.imageProperties.reservedLayoutSize = CGSize(width: UIListContentConfiguration.ImageProperties.standardDimension, height: 0)
|
||||||
|
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * self.avatarImageSize
|
||||||
|
self.contentConfiguration = config
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@objc private func preferencesChanged() {
|
||||||
|
guard var config = self.contentConfiguration as? UIListContentConfiguration else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config.imageProperties.cornerRadius = Preferences.shared.avatarStyle.cornerRadiusFraction * avatarImageSize
|
||||||
|
self.contentConfiguration = contentConfiguration
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -115,6 +115,12 @@ class MainSidebarViewController: UIViewController {
|
||||||
cell.contentConfiguration = config
|
cell.contentConfiguration = config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let myProfileCell = UICollectionView.CellRegistration<MainSidebarMyProfileCollectionViewCell, Item> { [unowned self] cell, indexPath, item in
|
||||||
|
Task {
|
||||||
|
await cell.updateUI(item: item, account: self.mastodonController.accountInfo!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
let outlineHeaderCell = UICollectionView.CellRegistration<UICollectionViewListCell, Item> { (cell, indexPath, item) in
|
||||||
var config = cell.defaultContentConfiguration()
|
var config = cell.defaultContentConfiguration()
|
||||||
config.attributedText = NSAttributedString(string: item.title, attributes: [
|
config.attributedText = NSAttributedString(string: item.title, attributes: [
|
||||||
|
@ -125,7 +131,9 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
return UICollectionViewDiffableDataSource<Section, Item>(collectionView: collectionView, cellProvider: { (collectionView, indexPath, item) -> UICollectionViewCell? in
|
||||||
if item.hasChildren {
|
if case .tab(.myProfile) = item {
|
||||||
|
return collectionView.dequeueConfiguredReusableCell(using: myProfileCell, for: indexPath, item: item)
|
||||||
|
} else if item.hasChildren {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCell, for: indexPath, item: item)
|
return collectionView.dequeueConfiguredReusableCell(using: outlineHeaderCell, for: indexPath, item: item)
|
||||||
} else {
|
} else {
|
||||||
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
return collectionView.dequeueConfiguredReusableCell(using: listCell, for: indexPath, item: item)
|
||||||
|
@ -322,6 +330,14 @@ class MainSidebarViewController: UIViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func myProfileCell() -> UICollectionViewCell? {
|
||||||
|
guard let indexPath = dataSource.indexPath(for: .tab(.myProfile)),
|
||||||
|
let item = collectionView.cellForItem(at: indexPath) else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return item
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSidebarViewController {
|
extension MainSidebarViewController {
|
||||||
|
@ -506,6 +522,11 @@ extension MainSidebarViewController: UICollectionViewDelegate {
|
||||||
let activity = userActivityForItem(item) else {
|
let activity = userActivityForItem(item) else {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
if case .tab(.myProfile) = item,
|
||||||
|
// only disable context menu on long-press, to allow fast account switching
|
||||||
|
collectionView.contextMenuInteraction?.menuAppearance == .rich {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) { (_) in
|
||||||
return UIMenu(children: [
|
return UIMenu(children: [
|
||||||
UIWindowScene.ActivationAction({ action in
|
UIWindowScene.ActivationAction({ action in
|
||||||
|
@ -522,6 +543,9 @@ extension MainSidebarViewController: UICollectionViewDragDelegate {
|
||||||
let activity = userActivityForItem(item) else {
|
let activity = userActivityForItem(item) else {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
|
if case .tab(.myProfile) = item {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
|
||||||
let provider = NSItemProvider(object: activity)
|
let provider = NSItemProvider(object: activity)
|
||||||
return [UIDragItem(itemProvider: provider)]
|
return [UIDragItem(itemProvider: provider)]
|
||||||
|
|
|
@ -13,6 +13,7 @@ class MainSplitViewController: UISplitViewController {
|
||||||
weak var mastodonController: MastodonController!
|
weak var mastodonController: MastodonController!
|
||||||
|
|
||||||
private var sidebar: MainSidebarViewController!
|
private var sidebar: MainSidebarViewController!
|
||||||
|
private var fastAccountSwitcher: FastAccountSwitcherViewController?
|
||||||
|
|
||||||
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
// Keep track of navigation stacks per-item so that we can only ever use a single navigation controller
|
||||||
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
private var navigationStacks: [MainSidebarViewController.Item: [UIViewController]] = [:]
|
||||||
|
@ -23,14 +24,6 @@ class MainSplitViewController: UISplitViewController {
|
||||||
viewController(for: .secondary) as? UINavigationController
|
viewController(for: .secondary) as? UINavigationController
|
||||||
}
|
}
|
||||||
|
|
||||||
override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
|
|
||||||
if UIDevice.current.userInterfaceIdiom == .phone {
|
|
||||||
return .portrait
|
|
||||||
} else {
|
|
||||||
return .all
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(mastodonController: MastodonController) {
|
init(mastodonController: MastodonController) {
|
||||||
self.mastodonController = mastodonController
|
self.mastodonController = mastodonController
|
||||||
|
|
||||||
|
@ -60,6 +53,18 @@ class MainSplitViewController: UISplitViewController {
|
||||||
select(item: .tab(.timelines))
|
select(item: .tab(.timelines))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if UIDevice.current.userInterfaceIdiom != .mac {
|
||||||
|
let switcher = FastAccountSwitcherViewController()
|
||||||
|
fastAccountSwitcher = switcher
|
||||||
|
switcher.itemOrientation = .iconsLeading
|
||||||
|
switcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
switcher.delegate = self
|
||||||
|
sidebar.view.addGestureRecognizer(switcher.createSwitcherGesture())
|
||||||
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(sidebarTapped))
|
||||||
|
tapRecognizer.cancelsTouchesInView = false
|
||||||
|
sidebar.view.addGestureRecognizer(tapRecognizer)
|
||||||
|
}
|
||||||
|
|
||||||
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
tabBarViewController = MainTabBarViewController(mastodonController: mastodonController)
|
||||||
setViewController(tabBarViewController, for: .compact)
|
setViewController(tabBarViewController, for: .compact)
|
||||||
|
|
||||||
|
@ -101,6 +106,10 @@ class MainSplitViewController: UISplitViewController {
|
||||||
select(item: item)
|
select(item: item)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@objc private func sidebarTapped() {
|
||||||
|
fastAccountSwitcher?.hide()
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: UISplitViewControllerDelegate {
|
extension MainSplitViewController: UISplitViewControllerDelegate {
|
||||||
|
@ -441,6 +450,10 @@ extension MainSplitViewController: TuskerRootViewController {
|
||||||
searchViewController.searchController.searchBar.text = query
|
searchViewController.searchController.searchBar.text = query
|
||||||
searchViewController.resultsController.performSearch(query: query)
|
searchViewController.resultsController.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentPreferences(completion: (() -> Void)?) {
|
||||||
|
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainSplitViewController: BackgroundableViewController {
|
extension MainSplitViewController: BackgroundableViewController {
|
||||||
|
@ -455,3 +468,27 @@ extension MainSplitViewController: BackgroundableViewController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
extension MainSplitViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
let currentAccount = fastAccountSwitcher.accountViews.first(where: \.isCurrent)!
|
||||||
|
let myProfileCell = sidebar.myProfileCell()!
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
currentAccount.centerYAnchor.constraint(equalTo: myProfileCell.centerYAnchor),
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: sidebar.view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
|
guard !isCollapsed,
|
||||||
|
let cell = sidebar.myProfileCell() else {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
let cellRect = cell.convert(cell.bounds, to: sidebar.view)
|
||||||
|
return cellRect.contains(point)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -60,13 +60,6 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
fastAccountSwitcher = FastAccountSwitcherViewController()
|
fastAccountSwitcher = FastAccountSwitcherViewController()
|
||||||
fastAccountSwitcher.delegate = self
|
fastAccountSwitcher.delegate = self
|
||||||
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
fastAccountSwitcher.view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(fastAccountSwitcher.view)
|
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
|
||||||
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
|
||||||
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
|
||||||
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
|
||||||
])
|
|
||||||
|
|
||||||
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
tabBar.addGestureRecognizer(fastAccountSwitcher.createSwitcherGesture())
|
||||||
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
let tapRecognizer = UITapGestureRecognizer(target: self, action: #selector(tabBarTapped))
|
||||||
|
@ -77,18 +70,17 @@ class MainTabBarViewController: UITabBarController, UITabBarControllerDelegate {
|
||||||
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
fastSwitcherIndicator = FastAccountSwitcherIndicatorView()
|
||||||
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
fastSwitcherIndicator.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.addSubview(fastSwitcherIndicator)
|
view.addSubview(fastSwitcherIndicator)
|
||||||
NSLayoutConstraint.activate([
|
|
||||||
fastSwitcherIndicator.widthAnchor.constraint(equalToConstant: 10),
|
|
||||||
fastSwitcherIndicator.heightAnchor.constraint(equalToConstant: 12),
|
|
||||||
])
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tabBar.isSpringLoaded = true
|
tabBar.isSpringLoaded = true
|
||||||
}
|
}
|
||||||
|
|
||||||
override func viewWillAppear(_ animated: Bool) {
|
override func viewDidLayoutSubviews() {
|
||||||
super.viewWillAppear(animated)
|
super.viewDidLayoutSubviews()
|
||||||
|
|
||||||
|
// i hate that we have to do this so often :S
|
||||||
|
// but doing it only in viewWillAppear makes it not appear initially
|
||||||
|
// doing it in viewWillAppear inside a DispatchQueue.main.async works initially but then it disappears when long-pressed
|
||||||
repositionFastSwitcherIndicator()
|
repositionFastSwitcherIndicator()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -201,11 +193,23 @@ extension MainTabBarViewController {
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
extension MainTabBarViewController: FastAccountSwitcherViewControllerDelegate {
|
||||||
|
func fastAccountSwitcherAddToViewHierarchy(_ fastAccountSwitcher: FastAccountSwitcherViewController) {
|
||||||
|
view.addSubview(fastAccountSwitcher.view)
|
||||||
|
NSLayoutConstraint.activate([
|
||||||
|
fastAccountSwitcher.accountsStack.bottomAnchor.constraint(equalTo: fastAccountSwitcher.view.bottomAnchor),
|
||||||
|
|
||||||
|
fastAccountSwitcher.view.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||||
|
fastAccountSwitcher.view.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||||
|
fastAccountSwitcher.view.topAnchor.constraint(equalTo: view.topAnchor),
|
||||||
|
fastAccountSwitcher.view.bottomAnchor.constraint(equalTo: tabBar.topAnchor),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
func fastAccountSwitcher(_ fastAccountSwitcher: FastAccountSwitcherViewController, triggerZoneContains point: CGPoint) -> Bool {
|
||||||
guard let myProfileButton = findMyProfileTabBarButton() else {
|
guard let myProfileButton = findMyProfileTabBarButton() else {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
let locationInButton = myProfileButton.convert(point, from: fastAccountSwitcher.view)
|
let locationInButton = myProfileButton.convert(point, from: tabBar)
|
||||||
return myProfileButton.bounds.contains(locationInButton)
|
return myProfileButton.bounds.contains(locationInButton)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -262,6 +266,10 @@ extension MainTabBarViewController: TuskerRootViewController {
|
||||||
exploreController.searchController.searchBar.text = query
|
exploreController.searchController.searchBar.text = query
|
||||||
exploreController.resultsController.performSearch(query: query)
|
exploreController.resultsController.performSearch(query: query)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func presentPreferences(completion: (() -> Void)?) {
|
||||||
|
present(PreferencesNavigationController(mastodonController: mastodonController), animated: true, completion: completion)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
extension MainTabBarViewController: BackgroundableViewController {
|
extension MainTabBarViewController: BackgroundableViewController {
|
||||||
|
|
|
@ -13,4 +13,5 @@ protocol TuskerRootViewController: UIViewController {
|
||||||
func select(tab: MainTabBarViewController.Tab)
|
func select(tab: MainTabBarViewController.Tab)
|
||||||
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
func getTabController(tab: MainTabBarViewController.Tab) -> UIViewController?
|
||||||
func performSearch(query: String)
|
func performSearch(query: String)
|
||||||
|
func presentPreferences(completion: (() -> Void)?)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,122 @@
|
||||||
|
//
|
||||||
|
// PostService.swift
|
||||||
|
// Tusker
|
||||||
|
//
|
||||||
|
// Created by Shadowfacts on 4/27/22.
|
||||||
|
// Copyright © 2022 Shadowfacts. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Pachyderm
|
||||||
|
|
||||||
|
class PostService: ObservableObject {
|
||||||
|
private let mastodonController: MastodonController
|
||||||
|
private let draft: Draft
|
||||||
|
let totalSteps: Int
|
||||||
|
|
||||||
|
@Published var currentStep = 1
|
||||||
|
|
||||||
|
init(mastodonController: MastodonController, draft: Draft) {
|
||||||
|
self.mastodonController = mastodonController
|
||||||
|
self.draft = draft
|
||||||
|
// 2 steps (request data, then upload) for each attachment
|
||||||
|
self.totalSteps = 2 + (draft.attachments.count * 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func post() async throws {
|
||||||
|
guard draft.hasContent else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// save before posting, so if a crash occurs during network request, the status won't be lost
|
||||||
|
DraftsManager.save()
|
||||||
|
|
||||||
|
let uploadedAttachments = try await uploadAttachments()
|
||||||
|
|
||||||
|
let contentWarning = draft.contentWarningEnabled ? draft.contentWarning : nil
|
||||||
|
let sensitive = contentWarning != nil
|
||||||
|
|
||||||
|
let request = Client.createStatus(
|
||||||
|
text: draft.textForPosting(on: mastodonController.instanceFeatures),
|
||||||
|
contentType: Preferences.shared.statusContentType,
|
||||||
|
inReplyTo: draft.inReplyToID,
|
||||||
|
media: uploadedAttachments,
|
||||||
|
sensitive: sensitive,
|
||||||
|
spoilerText: contentWarning,
|
||||||
|
visibility: draft.visibility,
|
||||||
|
language: nil,
|
||||||
|
pollOptions: draft.poll?.options.map(\.text),
|
||||||
|
pollExpiresIn: draft.poll == nil ? nil : Int(draft.poll!.duration),
|
||||||
|
pollMultiple: draft.poll?.multiple,
|
||||||
|
localOnly: mastodonController.instanceFeatures.localOnlyPosts ? draft.localOnly : nil
|
||||||
|
)
|
||||||
|
do {
|
||||||
|
let (_, _) = try await mastodonController.run(request)
|
||||||
|
currentStep += 1
|
||||||
|
|
||||||
|
DraftsManager.shared.remove(self.draft)
|
||||||
|
} catch let error as Client.Error {
|
||||||
|
throw Error.posting(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadAttachments() async throws -> [Attachment] {
|
||||||
|
var attachments: [Attachment] = []
|
||||||
|
attachments.reserveCapacity(draft.attachments.count)
|
||||||
|
for (index, attachment) in draft.attachments.enumerated() {
|
||||||
|
let data: Data
|
||||||
|
let mimeType: String
|
||||||
|
do {
|
||||||
|
(data, mimeType) = try await getData(for: attachment)
|
||||||
|
currentStep += 1
|
||||||
|
} catch let error as CompositionAttachmentData.Error {
|
||||||
|
throw Error.attachmentData(index: index, cause: error)
|
||||||
|
}
|
||||||
|
do {
|
||||||
|
let uploaded = try await uploadAttachment(data: data, mimeType: mimeType, description: attachment.description)
|
||||||
|
attachments.append(uploaded)
|
||||||
|
currentStep += 1
|
||||||
|
} catch let error as Client.Error {
|
||||||
|
throw Error.attachmentUpload(index: index, cause: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attachments
|
||||||
|
}
|
||||||
|
|
||||||
|
private func getData(for attachment: CompositionAttachment) async throws -> (Data, String) {
|
||||||
|
return try await withCheckedThrowingContinuation { continuation in
|
||||||
|
attachment.data.getData { result in
|
||||||
|
switch result {
|
||||||
|
case let .success(res):
|
||||||
|
continuation.resume(returning: res)
|
||||||
|
case let .failure(error):
|
||||||
|
continuation.resume(throwing: error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func uploadAttachment(data: Data, mimeType: String, description: String?) async throws -> Attachment {
|
||||||
|
let formAttachment = FormAttachment(mimeType: mimeType, data: data, fileName: "file")
|
||||||
|
let req = Client.upload(attachment: formAttachment, description: description)
|
||||||
|
return try await mastodonController.run(req).0
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Error: Swift.Error, LocalizedError {
|
||||||
|
case attachmentData(index: Int, cause: CompositionAttachmentData.Error)
|
||||||
|
case attachmentUpload(index: Int, cause: Client.Error)
|
||||||
|
case posting(Client.Error)
|
||||||
|
|
||||||
|
var localizedDescription: String {
|
||||||
|
switch self {
|
||||||
|
case let .attachmentData(index: index, cause: cause):
|
||||||
|
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
||||||
|
case let .attachmentUpload(index: index, cause: cause):
|
||||||
|
return "Attachment \(index + 1): \(cause.localizedDescription)"
|
||||||
|
case let .posting(error):
|
||||||
|
return error.localizedDescription
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,10 @@ import UIKit
|
||||||
|
|
||||||
class FastAccountSwitcherIndicatorView: UIView {
|
class FastAccountSwitcherIndicatorView: UIView {
|
||||||
|
|
||||||
|
override var intrinsicContentSize: CGSize {
|
||||||
|
CGSize(width: 10, height: 12)
|
||||||
|
}
|
||||||
|
|
||||||
override init(frame: CGRect) {
|
override init(frame: CGRect) {
|
||||||
super.init(frame: frame)
|
super.init(frame: frame)
|
||||||
|
|
||||||
|
|
|
@ -11,8 +11,8 @@ import SwiftUI
|
||||||
struct WrappedProgressView: UIViewRepresentable {
|
struct WrappedProgressView: UIViewRepresentable {
|
||||||
typealias UIViewType = UIProgressView
|
typealias UIViewType = UIProgressView
|
||||||
|
|
||||||
let value: Double
|
let value: Int
|
||||||
let total: Double
|
let total: Int
|
||||||
|
|
||||||
func makeUIView(context: Context) -> UIProgressView {
|
func makeUIView(context: Context) -> UIProgressView {
|
||||||
return UIProgressView(progressViewStyle: .bar)
|
return UIProgressView(progressViewStyle: .bar)
|
||||||
|
@ -20,7 +20,9 @@ struct WrappedProgressView: UIViewRepresentable {
|
||||||
|
|
||||||
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
func updateUIView(_ uiView: UIProgressView, context: Context) {
|
||||||
if total > 0 {
|
if total > 0 {
|
||||||
uiView.setProgress(Float(value / total), animated: true)
|
let progress = Float(value) / Float(total)
|
||||||
|
print(progress)
|
||||||
|
uiView.setProgress(progress, animated: true)
|
||||||
} else {
|
} else {
|
||||||
uiView.setProgress(0, animated: true)
|
uiView.setProgress(0, animated: true)
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue