Add share extension

This commit is contained in:
Marcin Czachursk 2023-04-07 13:20:37 +02:00
parent 4871867aca
commit 36f1a38a9d
8 changed files with 916 additions and 2 deletions

View File

@ -126,6 +126,11 @@
F88AB05829B36B8200345EDE /* AccountsPhotoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88AB05729B36B8200345EDE /* AccountsPhotoView.swift */; };
F88ABD9229686F1C004EF61E /* MemoryCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9129686F1C004EF61E /* MemoryCache.swift */; };
F88ABD9429687CA4004EF61E /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88ABD9329687CA4004EF61E /* ComposeView.swift */; };
F88BC50529E02F3900CE6141 /* ShareViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC50429E02F3900CE6141 /* ShareViewController.swift */; };
F88BC50C29E02F3900CE6141 /* VernissageShareExtension.appex in Embed Foundation Extensions */ = {isa = PBXBuildFile; fileRef = F88BC50229E02F3900CE6141 /* VernissageShareExtension.appex */; settings = {ATTRIBUTES = (RemoveHeadersOnCopy, ); }; };
F88BC51129E02F5300CE6141 /* PixelfedKit in Frameworks */ = {isa = PBXBuildFile; productRef = F88BC51029E02F5300CE6141 /* PixelfedKit */; };
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51229E02FD800CE6141 /* ComposeView.swift */; };
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88BC51529E0307F00CE6141 /* NotificationsName.swift */; };
F88C246C295C37B80006098B /* VernissageApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246B295C37B80006098B /* VernissageApp.swift */; };
F88C246E295C37B80006098B /* MainView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F88C246D295C37B80006098B /* MainView.swift */; };
F88C2470295C37BB0006098B /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = F88C246F295C37BB0006098B /* Assets.xcassets */; };
@ -235,6 +240,13 @@
remoteGlobalIDString = F864F75C29BB91B400B13921;
remoteInfo = VernissageWidgetExtension;
};
F88BC50A29E02F3900CE6141 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = F88C2460295C37B80006098B /* Project object */;
proxyType = 1;
remoteGlobalIDString = F88BC50129E02F3900CE6141;
remoteInfo = VernissageShareExtension;
};
/* End PBXContainerItemProxy section */
/* Begin PBXCopyFilesBuildPhase section */
@ -245,6 +257,7 @@
dstSubfolderSpec = 13;
files = (
F864F76C29BB91B600B13921 /* VernissageWidgetExtension.appex in Embed Foundation Extensions */,
F88BC50C29E02F3900CE6141 /* VernissageShareExtension.appex in Embed Foundation Extensions */,
);
name = "Embed Foundation Extensions";
runOnlyForDeploymentPostprocessing = 0;
@ -352,6 +365,12 @@
F88ABD9129686F1C004EF61E /* MemoryCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MemoryCache.swift; sourceTree = "<group>"; };
F88ABD9329687CA4004EF61E /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
F88ABD9529687D4D004EF61E /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
F88BC50229E02F3900CE6141 /* VernissageShareExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = VernissageShareExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; };
F88BC50429E02F3900CE6141 /* ShareViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ShareViewController.swift; sourceTree = "<group>"; };
F88BC50929E02F3900CE6141 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
F88BC51229E02FD800CE6141 /* ComposeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ComposeView.swift; sourceTree = "<group>"; };
F88BC51429E02FEB00CE6141 /* VernissageShareExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = VernissageShareExtension.entitlements; sourceTree = "<group>"; };
F88BC51529E0307F00CE6141 /* NotificationsName.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationsName.swift; sourceTree = "<group>"; };
F88C2468295C37B80006098B /* Vernissage.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Vernissage.app; sourceTree = BUILT_PRODUCTS_DIR; };
F88C246B295C37B80006098B /* VernissageApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = VernissageApp.swift; sourceTree = "<group>"; };
F88C246D295C37B80006098B /* MainView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainView.swift; sourceTree = "<group>"; };
@ -472,6 +491,14 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F88BC4FF29E02F3900CE6141 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
F88BC51129E02F5300CE6141 /* PixelfedKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F88C2465295C37B80006098B /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
@ -643,7 +670,6 @@
F83901A2295D863B00456AE2 /* Widgets */ = {
isa = PBXGroup;
children = (
F8864CF229AD05420020C534 /* TextView */,
F83901A5295D8EC000456AE2 /* LabelIcon.swift */,
F85D4972296406E700751DF7 /* BottomRight.swift */,
F85D497629640A5200751DF7 /* ImageRow.swift */,
@ -788,6 +814,18 @@
path = Cache;
sourceTree = "<group>";
};
F88BC50329E02F3900CE6141 /* VernissageShareExtension */ = {
isa = PBXGroup;
children = (
F88BC51429E02FEB00CE6141 /* VernissageShareExtension.entitlements */,
F88BC51229E02FD800CE6141 /* ComposeView.swift */,
F88BC51529E0307F00CE6141 /* NotificationsName.swift */,
F88BC50429E02F3900CE6141 /* ShareViewController.swift */,
F88BC50929E02F3900CE6141 /* Info.plist */,
);
path = VernissageShareExtension;
sourceTree = "<group>";
};
F88C245F295C37B80006098B = {
isa = PBXGroup;
children = (
@ -796,12 +834,14 @@
F844F42429D2DC39000DD896 /* LICENSE */,
F8B3699A29D86EB600BE3808 /* .swiftlint.yml */,
F8B3699B29D86EBD00BE3808 /* .gitignore */,
F8864CF229AD05420020C534 /* TextView */,
F8CB3DF029D80B1E00CDAE5A /* Resources */,
F835081F29BEF88600DE3247 /* Localization */,
F864F79C29BB9D2400B13921 /* Models */,
F8341F96295C6427009C8EE6 /* CoreData */,
F88C246A295C37B80006098B /* Vernissage */,
F864F76229BB91B400B13921 /* VernissageWidget */,
F88BC50329E02F3900CE6141 /* VernissageShareExtension */,
F88C2469295C37B80006098B /* Products */,
F89992C5296D3DF8005994BF /* Frameworks */,
);
@ -812,6 +852,7 @@
children = (
F88C2468295C37B80006098B /* Vernissage.app */,
F864F75D29BB91B400B13921 /* VernissageWidgetExtension.appex */,
F88BC50229E02F3900CE6141 /* VernissageShareExtension.appex */,
);
name = Products;
sourceTree = "<group>";
@ -1006,6 +1047,26 @@
productReference = F864F75D29BB91B400B13921 /* VernissageWidgetExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
F88BC50129E02F3900CE6141 /* VernissageShareExtension */ = {
isa = PBXNativeTarget;
buildConfigurationList = F88BC50F29E02F3900CE6141 /* Build configuration list for PBXNativeTarget "VernissageShareExtension" */;
buildPhases = (
F88BC4FE29E02F3900CE6141 /* Sources */,
F88BC4FF29E02F3900CE6141 /* Frameworks */,
F88BC50029E02F3900CE6141 /* Resources */,
);
buildRules = (
);
dependencies = (
);
name = VernissageShareExtension;
packageProductDependencies = (
F88BC51029E02F5300CE6141 /* PixelfedKit */,
);
productName = VernissageShareExtension;
productReference = F88BC50229E02F3900CE6141 /* VernissageShareExtension.appex */;
productType = "com.apple.product-type.app-extension";
};
F88C2467295C37B80006098B /* Vernissage */ = {
isa = PBXNativeTarget;
buildConfigurationList = F88C247B295C37BB0006098B /* Build configuration list for PBXNativeTarget "Vernissage" */;
@ -1020,6 +1081,7 @@
);
dependencies = (
F864F76B29BB91B600B13921 /* PBXTargetDependency */,
F88BC50B29E02F3900CE6141 /* PBXTargetDependency */,
);
name = Vernissage;
packageProductDependencies = (
@ -1043,12 +1105,15 @@
isa = PBXProject;
attributes = {
BuildIndependentTargetsInParallel = 1;
LastSwiftUpdateCheck = 1420;
LastSwiftUpdateCheck = 1430;
LastUpgradeCheck = 1420;
TargetAttributes = {
F864F75C29BB91B400B13921 = {
CreatedOnToolsVersion = 14.2;
};
F88BC50129E02F3900CE6141 = {
CreatedOnToolsVersion = 14.3;
};
F88C2467295C37B80006098B = {
CreatedOnToolsVersion = 14.2;
};
@ -1078,6 +1143,7 @@
targets = (
F88C2467295C37B80006098B /* Vernissage */,
F864F75C29BB91B400B13921 /* VernissageWidgetExtension */,
F88BC50129E02F3900CE6141 /* VernissageShareExtension */,
);
};
/* End PBXProject section */
@ -1092,6 +1158,13 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F88BC50029E02F3900CE6141 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
F88C2466295C37B80006098B /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -1164,6 +1237,16 @@
);
runOnlyForDeploymentPostprocessing = 0;
};
F88BC4FE29E02F3900CE6141 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
F88BC50529E02F3900CE6141 /* ShareViewController.swift in Sources */,
F88BC51629E0307F00CE6141 /* NotificationsName.swift in Sources */,
F88BC51329E02FD800CE6141 /* ComposeView.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
F88C2464295C37B80006098B /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -1348,6 +1431,11 @@
target = F864F75C29BB91B400B13921 /* VernissageWidgetExtension */;
targetProxy = F864F76A29BB91B600B13921 /* PBXContainerItemProxy */;
};
F88BC50B29E02F3900CE6141 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = F88BC50129E02F3900CE6141 /* VernissageShareExtension */;
targetProxy = F88BC50A29E02F3900CE6141 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */
/* Begin PBXVariantGroup section */
@ -1421,6 +1509,61 @@
};
name = Release;
};
F88BC50D29E02F3900CE6141 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = VernissageShareExtension/VernissageShareExtension.entitlements;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 96;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Debug;
};
F88BC50E29E02F3900CE6141 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_ENTITLEMENTS = VernissageShareExtension/VernissageShareExtension.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 96;
DEVELOPMENT_TEAM = B2U9FEKYP8;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = VernissageShareExtension/Info.plist;
INFOPLIST_KEY_CFBundleDisplayName = VernissageShareExtension;
INFOPLIST_KEY_NSHumanReadableCopyright = "";
IPHONEOS_DEPLOYMENT_TARGET = 16.4;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@executable_path/../../Frameworks",
);
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = dev.mczachurski.vernissage.share;
PRODUCT_NAME = "$(TARGET_NAME)";
SKIP_INSTALL = YES;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = 1;
};
name = Release;
};
F88C2479295C37BB0006098B /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -1629,6 +1772,15 @@
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F88BC50F29E02F3900CE6141 /* Build configuration list for PBXNativeTarget "VernissageShareExtension" */ = {
isa = XCConfigurationList;
buildConfigurations = (
F88BC50D29E02F3900CE6141 /* Debug */,
F88BC50E29E02F3900CE6141 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
F88C2463295C37B80006098B /* Build configuration list for PBXProject "Vernissage" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1721,6 +1873,10 @@
isa = XCSwiftPackageProductDependency;
productName = PixelfedKit;
};
F88BC51029E02F5300CE6141 /* PixelfedKit */ = {
isa = XCSwiftPackageProductDependency;
productName = PixelfedKit;
};
F88E4D4C297EA4290057491A /* EmojiText */ = {
isa = XCSwiftPackageProductDependency;
package = F88E4D4B297EA4290057491A /* XCRemoteSwiftPackageReference "EmojiText" */;

View File

@ -0,0 +1,662 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
import SwiftUI
struct ComposeView: View {
var body: some View {
VStack {
Text("Hello!")
Button("Close") {
NotificationCenter.default.post(name: NotificationsName.shareSheetClose, object: nil)
}
.buttonStyle(.borderedProminent)
}
}
}
//public class PhotoAttachment: ObservableObject, Identifiable, Equatable, Hashable {
// public let id: String
// public let photosPickerItem: PhotosPickerItem
//
// @Published public var photoData: Data?
// @Published public var uploadedAttachment: UploadedAttachment?
// @Published public var error: Error?
//
// init(photosPickerItem: PhotosPickerItem) {
// self.id = UUID().uuidString
// self.photosPickerItem = photosPickerItem
// }
//
// public static func == (lhs: PhotoAttachment, rhs: PhotoAttachment) -> Bool {
// lhs.id == rhs.id
// }
//
// public func hash(into hasher: inout Hasher) {
// return hasher.combine(self.id)
// }
//}
//
//extension [PhotoAttachment] {
// public func hasUploadedPhotos() -> Bool {
// return self.contains { photoAttachment in
// photoAttachment.uploadedAttachment != nil
// }
// }
//
// public func getUploadedPhotoIds() -> [String] {
// var ids: [String] = []
//
// for item in self {
// if let uploadedAttachment = item.uploadedAttachment {
// ids.append(uploadedAttachment.id)
// }
// }
//
// return ids
// }
//}
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
/*
import SwiftUI
import PhotosUI
import PixelfedKit
import UIKit
struct ComposeView: View {
@StateObject private var textModel: TextModel
@State private var isKeyboardPresented = false
@State private var isSensitive = false
@State private var spoilerText = ""
@State private var commentsDisabled = false
@State private var place: Place?
@State private var photosAreAttached = false
@State private var publishDisabled = true
@State private var interactiveDismissDisabled = false
@State private var photosAreUploading = false
@State private var photosPickerVisible = false
@State private var selectedItems: [PhotosPickerItem] = []
@State private var photosAttachment: [PhotoAttachment] = []
@State private var visibility = Pixelfed.Statuses.Visibility.pub
@State private var visibilityText: LocalizedStringKey = "compose.title.everyone"
@State private var visibilityImage = "globe.europe.africa"
@FocusState private var focusedField: FocusField?
enum FocusField: Hashable {
case unknown
case content
case spoilerText
}
@State private var showSheet: SheetType?
enum SheetType: Identifiable {
case photoDetails(PhotoAttachment)
case placeSelector
public var id: String {
switch self {
case .photoDetails:
return "photoDetails"
case .placeSelector:
return "placeSelector"
}
}
}
private let keyboardFontImageSize = 20.0
private let keyboardFontTextSize = 16.0
private let autocompleteFontTextSize = 12.0
public init() {
_textModel = StateObject(wrappedValue: .init())
}
var body: some View {
NavigationStack {
NavigationView {
ZStack(alignment: .bottom) {
self.composeBody()
if self.isKeyboardPresented {
VStack(alignment: .leading, spacing: 0) {
self.autocompleteToolbar()
self.keyboardToolbar()
}
.transition(.opacity)
}
}
.frame(alignment: .topLeading)
.toolbar {
ToolbarItem(placement: .primaryAction) {
Button {
Task {
await self.publishStatus()
}
} label: {
Text("compose.title.publish", comment: "Publish")
}
.disabled(self.publishDisabled)
.buttonStyle(.borderedProminent)
}
ToolbarItem(placement: .cancellationAction) {
Button(NSLocalizedString("compose.title.cancel", comment: "Cancel"), role: .cancel) {
NotificationCenter.default.post(name: NotificationsName.shareSheetClose, object: nil)
}
}
}
.onAppear {
self.textModel.client = self.client
}
.onChange(of: self.textModel.text) { _ in
self.refreshScreenState()
}
.onChange(of: self.selectedItems) { _ in
Task {
await self.loadPhotos()
}
}
.sheet(item: $showSheet, content: { sheetType in
switch sheetType {
case .photoDetails(let photoAttachment):
// TODO: Move to common views?
PhotoEditorView(photoAttachment: photoAttachment)
case .placeSelector:
// TODO: Move to common views?
PlaceSelectorView(place: $place)
}
})
.onReceive(keyboardPublisher) { value in
withAnimation {
self.isKeyboardPresented = value
}
}
.photosPicker(isPresented: $photosPickerVisible,
selection: $selectedItems,
maxSelectionCount: self.applicationState.statusMaxMediaAttachments,
matching: .images)
.navigationTitle("compose.navigationBar.title")
.navigationBarTitleDisplayMode(.inline)
}
.withAppRouteur()
.withOverlayDestinations(overlayDestinations: $routerPath.presentedOverlay)
}
.interactiveDismissDisabled(self.interactiveDismissDisabled)
}
@ViewBuilder
private func composeBody() -> some View {
ScrollView {
VStack(alignment: .leading) {
// Red content warning.
self.contentWarningView()
// Information that comments are disabled.
self.commentsDisabledView()
// User avatar and name.
self.userAvatarView()
// Incofmation about status visibility.
self.visibilityComboView()
// Text area with new status.
self.statusTextView()
// Grid with images.
self.imagesGridView()
// Status when we are adding new comment.
self.statusModelView()
Spacer()
}
}
}
@ViewBuilder
private func imagesGridView() -> some View {
HStack(alignment: .center) {
LazyVGrid(columns: [GridItem(.adaptive(minimum: 80))]) {
ForEach(self.photosAttachment, id: \.id) { photoAttachment in
ImageUploadView(photoAttachment: photoAttachment) {
self.showSheet = .photoDetails(photoAttachment)
} delete: {
self.photosAttachment = self.photosAttachment.filter({ item in
item != photoAttachment
})
self.selectedItems = self.selectedItems.filter({ item in
item != photoAttachment.photosPickerItem
})
self.refreshScreenState()
} upload: {
Task {
photoAttachment.error = nil
await self.upload(photoAttachment)
self.refreshScreenState()
}
}
}
}
}
.padding(8)
}
@ViewBuilder
private func statusModelView() -> some View {
if let status = self.statusViewModel {
HStack(alignment: .top) {
UserAvatar(accountAvatar: status.account.avatar, size: .comment)
VStack(alignment: .leading, spacing: 0) {
HStack(alignment: .top) {
Text(statusViewModel?.account.displayNameWithoutEmojis ?? "")
.foregroundColor(.mainTextColor)
.font(.footnote)
.fontWeight(.bold)
Spacer()
}
MarkdownFormattedText(status.content.asMarkdown)
.font(.subheadline)
.environment(\.openURL, OpenURLAction { _ in .handled })
}
}
.padding(8)
.background(Color.selectedRowColor)
}
}
@ViewBuilder
private func statusTextView() -> some View {
TextView($textModel.text, getTextView: { textView in
self.textModel.textView = textView
})
.placeholder(self.placeholder())
.padding(.horizontal, 8)
.focused($focusedField, equals: .content)
.onFirstAppear {
self.focusedField = .content
}
}
@ViewBuilder
private func userAvatarView() -> some View {
if let accountData = applicationState.account {
HStack {
UsernameRow(
accountId: accountData.id,
accountAvatar: accountData.avatar,
accountDisplayName: accountData.displayName,
accountUsername: accountData.username)
Spacer()
}
.padding(.horizontal, 8)
}
}
@ViewBuilder
private func contentWarningView() -> some View {
if self.isSensitive {
TextField("compose.title.writeContentWarning", text: $spoilerText, axis: .vertical)
.padding(8)
.lineLimit(1...2)
.focused($focusedField, equals: .spoilerText)
.keyboardType(.default)
.background(Color.dangerColor.opacity(0.4))
}
}
@ViewBuilder
private func commentsDisabledView() -> some View {
if self.commentsDisabled {
HStack {
Spacer()
Text("compose.title.commentsWillBeDisabled")
.textCase(.uppercase)
.font(.caption2)
.foregroundColor(.dangerColor)
}
.padding(.horizontal, 8)
}
}
@ViewBuilder
private func visibilityComboView() -> some View {
HStack {
Menu {
Button {
self.visibility = .pub
self.visibilityText = "compose.title.everyone"
self.visibilityImage = "globe.europe.africa"
} label: {
Label("compose.title.everyone", systemImage: "globe.europe.africa")
}
Button {
self.visibility = .unlisted
self.visibilityText = "compose.title.unlisted"
self.visibilityImage = "lock.open"
} label: {
Label("compose.title.unlisted", systemImage: "lock.open")
}
Button {
self.visibility = .priv
self.visibilityText = "compose.title.followers"
self.visibilityImage = "lock"
} label: {
Label("compose.title.followers", systemImage: "lock")
}
} label: {
HStack {
Label(self.visibilityText, systemImage: self.visibilityImage)
Image(systemName: "chevron.down")
}
.padding(.vertical, 4)
.padding(.horizontal, 8)
.overlay(
RoundedRectangle(cornerRadius: 8)
.stroke(Color.accentColor, lineWidth: 1)
)
}
Spacer()
if let name = self.place?.name, let country = self.place?.country {
Group {
Image(systemName: "mappin.and.ellipse")
Text("\(name), \(country)")
}
.foregroundColor(.lightGrayColor)
.padding(.trailing, 8)
}
}
.font(.footnote)
.padding(.horizontal, 8)
}
@ViewBuilder
private func autocompleteToolbar() -> some View {
if !textModel.mentionsSuggestions.isEmpty || !textModel.tagsSuggestions.isEmpty {
ScrollView(.horizontal, showsIndicators: false) {
LazyHStack {
if !textModel.mentionsSuggestions.isEmpty {
ForEach(textModel.mentionsSuggestions, id: \.id) { account in
Button {
textModel.selectMentionSuggestion(account: account)
} label: {
HStack(alignment: .center) {
UserAvatar(accountAvatar: account.avatar, size: .comment)
VStack(alignment: .leading) {
Text(account.displayNameWithoutEmojis)
.foregroundColor(.mainTextColor)
Text("@\(account.acct)")
.foregroundColor(.lightGrayColor)
}
.padding(.leading, 8)
}
.font(.system(size: self.autocompleteFontTextSize))
.padding(.trailing, 8)
}
Divider()
}
} else {
ForEach(textModel.tagsSuggestions, id: \.url) { tag in
Button {
textModel.selectHashtagSuggestion(tag: tag)
} label: {
Text("#\(tag.name)")
.font(.system(size: self.autocompleteFontTextSize))
.foregroundColor(self.applicationState.tintColor.color())
}
Divider()
}
}
}
.padding(.horizontal, 8)
}
.frame(height: 40)
.background(.ultraThinMaterial)
}
}
@ViewBuilder
private func keyboardToolbar() -> some View {
VStack(spacing: 0) {
Divider()
HStack(alignment: .center, spacing: 22) {
Button {
hideKeyboard()
self.focusedField = .unknown
self.photosPickerVisible = true
} label: {
Image(systemName: self.photosAreAttached ? "photo.fill.on.rectangle.fill" : "photo.on.rectangle")
}
Button {
withAnimation(.easeInOut) {
self.isSensitive.toggle()
if self.isSensitive {
self.focusedField = .spoilerText
} else {
self.focusedField = .content
}
}
} label: {
Image(systemName: self.isSensitive ? "exclamationmark.square.fill" : "exclamationmark.square")
}
Button {
withAnimation(.easeInOut) {
self.commentsDisabled.toggle()
}
} label: {
Image(systemName: self.commentsDisabled ? "person.2.slash" : "person.2.fill")
}
Button {
if self.place != nil {
withAnimation(.easeInOut) {
self.place = nil
}
} else {
self.showSheet = .placeSelector
}
} label: {
Image(systemName: self.place == nil ? "mappin.square" : "mappin.square.fill")
}
Button {
self.textModel.insertAtCursorPosition(content: "#")
} label: {
Image(systemName: "number")
}
Button {
self.textModel.insertAtCursorPosition(content: "@")
} label: {
Image(systemName: "at")
}
Spacer()
Text("\(self.applicationState.statusMaxCharacters - textModel.text.string.utf16.count)")
.foregroundColor(.lightGrayColor)
.font(.system(size: self.keyboardFontTextSize))
}
.padding(8)
.font(.system(size: self.keyboardFontImageSize))
}
.background(Color.keyboardToolbarColor)
}
private func placeholder() -> LocalizedStringKey {
self.statusViewModel == nil ? "compose.title.attachPhotoFull" : "compose.title.attachPhotoMini"
}
private func isPublishButtonDisabled() -> Bool {
// Publish always disabled when there is not status text.
if self.textModel.text.string.isEmpty {
return true
}
// When application is during uploading photos we cannot send new status.
if self.photosAreUploading == true {
return true
}
// When status is not a comment, then photo is required.
if self.statusViewModel == nil && self.photosAttachment.hasUploadedPhotos() == false {
return true
}
return false
}
private func isInteractiveDismissDisabled() -> Bool {
if self.textModel.text.string.isEmpty == false {
return true
}
if self.photosAreUploading == true {
return true
}
if self.photosAttachment.hasUploadedPhotos() == true {
return true
}
return false
}
private func loadPhotos() async {
do {
self.photosAreUploading = true
self.publishDisabled = self.isPublishButtonDisabled()
self.interactiveDismissDisabled = self.isInteractiveDismissDisabled()
// We have to create list with existing photos.
var temporaryPhotosAttachment: [PhotoAttachment] = []
for item in self.selectedItems {
if let photoAttachment = self.photosAttachment.first(where: { $0.photosPickerItem == item }) {
temporaryPhotosAttachment.append(photoAttachment)
continue
}
temporaryPhotosAttachment.append(PhotoAttachment(photosPickerItem: item))
}
// We can show new list on the screen.
self.photosAttachment = temporaryPhotosAttachment
// Now we have to get from photos images as JPEG.
for item in self.photosAttachment.filter({ $0.photoData == nil }) {
if let data = try await item.photosPickerItem.loadTransferable(type: Data.self) {
item.photoData = data
}
}
// Open again the keyboard.
self.focusedField = .content
// Upload images which hasn't been uploaded yet.
await self.upload()
// Change state of the screen.
self.photosAreUploading = false
self.refreshScreenState()
} catch {
ErrorService.shared.handle(error, message: "compose.error.loadingPhotosFailed", showToastr: true)
}
}
private func refreshScreenState() {
self.photosAreAttached = self.photosAttachment.hasUploadedPhotos()
self.publishDisabled = self.isPublishButtonDisabled()
self.interactiveDismissDisabled = self.isInteractiveDismissDisabled()
}
private func upload() async {
for photoAttachment in self.photosAttachment {
await self.upload(photoAttachment)
}
}
private func upload(_ photoAttachment: PhotoAttachment) async {
do {
// We have to have binary data and image shouldn't be uploaded yet.
guard let photoData = photoAttachment.photoData, photoAttachment.uploadedAttachment == nil else {
return
}
guard let image = UIImage(data: photoData) else {
return
}
guard let data = image.getJpegData() else {
return
}
let fileIndex = String.randomString(length: 8)
if let mediaAttachment = try await self.client.media?.upload(data: data,
fileName: "file-\(fileIndex).jpg",
mimeType: "image/jpeg") {
photoAttachment.uploadedAttachment = mediaAttachment
}
} catch {
photoAttachment.error = error
ErrorService.shared.handle(error, message: "compose.error.postingPhotoFailed", showToastr: true)
}
}
private func publishStatus() async {
do {
let status = self.createStatus()
if let newStatus = try await self.client.statuses?.new(status: status) {
ToastrService.shared.showSuccess("compose.title.statusPublished", imageSystemName: "message.fill")
let statusModel = StatusModel(status: newStatus)
let commentModel = CommentModel(status: statusModel, showDivider: false)
self.applicationState.newComment = commentModel
dismiss()
}
} catch {
ErrorService.shared.handle(error, message: "compose.error.postingStatusFailed", showToastr: true)
}
}
private func createStatus() -> Pixelfed.Statuses.Components {
return Pixelfed.Statuses.Components(inReplyToId: self.statusViewModel?.id,
text: self.textModel.text.string,
spoilerText: self.isSensitive ? self.spoilerText : String.empty(),
mediaIds: self.photosAttachment.getUploadedPhotoIds(),
visibility: self.visibility,
sensitive: self.isSensitive,
placeId: self.place?.id,
commentsDisabled: self.commentsDisabled)
}
}
*/

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSExtension</key>
<dict>
<key>NSExtensionAttributes</key>
<dict>
<key>NSExtensionActivationRule</key>
<dict>
<key>NSExtensionActivationSupportsImageWithMaxCount</key>
<string>4</string>
<key>NSExtensionActivationSupportsMovieWithMaxCount</key>
<string>0</string>
<key>NSExtensionActivationSupportsText</key>
<string>NO</string>
<key>NSExtensionActivationSupportsWebPageWithMaxCount</key>
<string>1</string>
<key>NSExtensionActivationSupportsWebURLWithMaxCount</key>
<string>1</string>
</dict>
</dict>
<key>NSExtensionPointIdentifier</key>
<string>com.apple.share-services</string>
<key>NSExtensionPrincipalClass</key>
<string>$(PRODUCT_MODULE_NAME).ShareViewController</string>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,11 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import Foundation
public enum NotificationsName {
public static let shareSheetClose = NSNotification.Name("shareSheetClose")
}

View File

@ -0,0 +1,46 @@
//
// https://mczachurski.dev
// Copyright © 2023 Marcin Czachurski and the repository contributors.
// Licensed under the Apache License 2.0.
//
import SwiftUI
import UIKit
import Social
class ShareViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
if let item = extensionContext?.inputItems.first as? NSExtensionItem {
let view = ComposeView()
let childView = UIHostingController(rootView: view)
addChild(childView)
childView.view.frame = self.view.bounds
self.view.addSubview(childView.view)
childView.didMove(toParent: self)
childView.view.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
childView.view.topAnchor.constraint(equalTo: self.view.topAnchor),
childView.view.bottomAnchor.constraint(equalTo: self.view.bottomAnchor),
childView.view.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
childView.view.trailingAnchor.constraint(equalTo: self.view.trailingAnchor)
])
}
NotificationCenter.default.addObserver(forName: NotificationsName.shareSheetClose, object: nil, queue: nil) { _ in
self.close()
}
}
func close() {
extensionContext?.completeRequest(returningItems: [], completionHandler: nil)
}
override func viewDidAppear(_ animated: Bool) {
super.viewDidAppear(animated)
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.application-groups</key>
<array>
<string>group.dev.mczachurski.vernissage</string>
</array>
</dict>
</plist>