2022-12-25 05:55:33 +00:00
|
|
|
import Accounts
|
2023-01-17 10:36:01 +00:00
|
|
|
import AppAccount
|
2022-12-25 05:55:33 +00:00
|
|
|
import DesignSystem
|
2023-01-17 10:36:01 +00:00
|
|
|
import EmojiText
|
|
|
|
import Env
|
2022-12-25 07:17:16 +00:00
|
|
|
import Models
|
|
|
|
import Network
|
2022-12-27 15:16:25 +00:00
|
|
|
import NukeUI
|
2023-01-17 10:36:01 +00:00
|
|
|
import PhotosUI
|
2023-02-18 06:26:48 +00:00
|
|
|
import StoreKit
|
2023-01-17 10:36:01 +00:00
|
|
|
import SwiftUI
|
2023-01-15 15:39:08 +00:00
|
|
|
import UIKit
|
2022-12-23 09:41:55 +00:00
|
|
|
|
2023-09-18 19:03:52 +00:00
|
|
|
@MainActor
|
2022-12-23 09:41:55 +00:00
|
|
|
public struct StatusEditorView: View {
|
2023-09-18 05:01:23 +00:00
|
|
|
@Environment(AppAccountsManager.self) private var appAccounts
|
2023-09-19 07:18:20 +00:00
|
|
|
@Environment(UserPreferences.self) private var preferences
|
2023-09-18 19:03:52 +00:00
|
|
|
@Environment(Theme.self) private var theme
|
2023-09-18 05:01:23 +00:00
|
|
|
@Environment(Client.self) private var client
|
|
|
|
@Environment(CurrentAccount.self) private var currentAccount
|
2022-12-23 09:41:55 +00:00
|
|
|
@Environment(\.dismiss) private var dismiss
|
2023-09-22 07:31:35 +00:00
|
|
|
@Environment(\.modelContext) private var context
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-09-18 05:01:23 +00:00
|
|
|
@State private var viewModel: StatusEditorViewModel
|
2022-12-28 09:45:05 +00:00
|
|
|
@FocusState private var isSpoilerTextFocused: Bool
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-11 11:44:34 +00:00
|
|
|
@State private var isDismissAlertPresented: Bool = false
|
2023-02-12 17:24:09 +00:00
|
|
|
@State private var isLanguageConfirmPresented = false
|
2023-10-16 07:16:17 +00:00
|
|
|
|
|
|
|
@State private var editingContainer: StatusEditorMediaContainer?
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-26 07:24:55 +00:00
|
|
|
public init(mode: StatusEditorViewModel.Mode) {
|
2023-09-18 05:01:23 +00:00
|
|
|
_viewModel = .init(initialValue: .init(mode: mode))
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-23 09:41:55 +00:00
|
|
|
public var body: some View {
|
|
|
|
NavigationStack {
|
2022-12-25 07:17:16 +00:00
|
|
|
ZStack(alignment: .bottom) {
|
2022-12-27 12:38:10 +00:00
|
|
|
ScrollView {
|
2022-12-27 18:10:31 +00:00
|
|
|
Divider()
|
2022-12-28 09:45:05 +00:00
|
|
|
spoilerTextView
|
2022-12-27 12:38:10 +00:00
|
|
|
VStack(spacing: 12) {
|
|
|
|
accountHeaderView
|
2023-01-03 06:41:29 +00:00
|
|
|
.padding(.horizontal, .layoutPadding)
|
2023-02-06 11:24:57 +00:00
|
|
|
TextView($viewModel.statusText,
|
|
|
|
getTextView: { textView in
|
2023-02-12 15:29:41 +00:00
|
|
|
viewModel.textView = textView
|
|
|
|
})
|
|
|
|
.placeholder(String(localized: "status.editor.text.placeholder"))
|
|
|
|
.setKeyboardType(preferences.isSocialKeyboardEnabled ? .twitter : .default)
|
|
|
|
.padding(.horizontal, .layoutPadding)
|
2023-10-16 07:16:17 +00:00
|
|
|
StatusEditorMediaView(viewModel: viewModel,
|
|
|
|
editingContainer: $editingContainer)
|
2023-01-17 14:14:50 +00:00
|
|
|
if let status = viewModel.embeddedStatus {
|
2023-09-19 07:25:48 +00:00
|
|
|
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
2023-01-03 06:41:29 +00:00
|
|
|
.padding(.horizontal, .layoutPadding)
|
2023-01-05 17:54:18 +00:00
|
|
|
.disabled(true)
|
|
|
|
} else if let status = viewModel.replyToStatus {
|
|
|
|
Divider()
|
|
|
|
.padding(.top, 20)
|
2023-09-19 07:25:48 +00:00
|
|
|
StatusEmbeddedView(status: status, client: client, routerPath: RouterPath())
|
2023-01-05 17:54:18 +00:00
|
|
|
.padding(.horizontal, .layoutPadding)
|
|
|
|
.disabled(true)
|
2022-12-27 12:38:10 +00:00
|
|
|
}
|
2023-01-13 06:30:15 +00:00
|
|
|
if viewModel.showPoll {
|
|
|
|
StatusEditorPollView(viewModel: viewModel, showPoll: $viewModel.showPoll)
|
|
|
|
.padding(.horizontal)
|
|
|
|
}
|
2022-12-27 12:38:10 +00:00
|
|
|
Spacer()
|
|
|
|
}
|
2022-12-27 18:10:31 +00:00
|
|
|
.padding(.top, 8)
|
2022-12-31 08:10:27 +00:00
|
|
|
.padding(.bottom, 40)
|
2023-09-17 06:47:15 +00:00
|
|
|
}
|
|
|
|
.accessibilitySortPriority(1) // Ensure that all elements inside the `ScrollView` occur earlier than the accessory views
|
|
|
|
.padding(.top, 1) // hacky fix for weird SwiftUI scrollView bug when adding padding
|
|
|
|
.padding(.bottom, 48)
|
2022-12-31 08:10:27 +00:00
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
|
|
StatusEditorAutoCompleteView(viewModel: viewModel)
|
|
|
|
StatusEditorAccessoryView(isSpoilerTextFocused: $isSpoilerTextFocused,
|
|
|
|
viewModel: viewModel)
|
2022-12-25 07:17:16 +00:00
|
|
|
}
|
|
|
|
}
|
2023-01-17 12:02:05 +00:00
|
|
|
.onDrop(of: StatusEditorUTTypeSupported.types(), delegate: viewModel)
|
2022-12-25 07:17:16 +00:00
|
|
|
.onAppear {
|
|
|
|
viewModel.client = client
|
2022-12-30 21:49:09 +00:00
|
|
|
viewModel.currentAccount = currentAccount.account
|
2022-12-31 11:11:42 +00:00
|
|
|
viewModel.theme = theme
|
2023-01-24 20:34:16 +00:00
|
|
|
viewModel.preferences = preferences
|
2022-12-26 07:24:55 +00:00
|
|
|
viewModel.prepareStatusText()
|
2022-12-27 07:31:57 +00:00
|
|
|
if !client.isAuth {
|
|
|
|
dismiss()
|
2023-01-15 15:39:08 +00:00
|
|
|
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
|
|
|
object: nil)
|
2022-12-27 07:31:57 +00:00
|
|
|
}
|
2023-01-22 05:38:30 +00:00
|
|
|
|
2023-01-18 18:11:52 +00:00
|
|
|
Task {
|
|
|
|
await viewModel.fetchCustomEmojis()
|
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
2023-09-18 05:01:23 +00:00
|
|
|
.onChange(of: currentAccount.account?.id) {
|
2023-01-18 07:07:09 +00:00
|
|
|
viewModel.currentAccount = currentAccount.account
|
2023-09-18 05:01:23 +00:00
|
|
|
}
|
2022-12-29 09:39:34 +00:00
|
|
|
.background(theme.primaryBackgroundColor)
|
2022-12-26 07:24:55 +00:00
|
|
|
.navigationTitle(viewModel.mode.title)
|
2022-12-23 09:41:55 +00:00
|
|
|
.navigationBarTitleDisplayMode(.inline)
|
2023-09-16 12:02:50 +00:00
|
|
|
.toolbarBackground(.visible, for: .navigationBar)
|
2023-07-05 07:03:28 +00:00
|
|
|
.alert("status.error.posting.title",
|
2023-01-22 08:09:35 +00:00
|
|
|
isPresented: $viewModel.showPostingErrorAlert,
|
|
|
|
actions: {
|
2023-07-05 07:03:28 +00:00
|
|
|
Button("OK") {}
|
2023-01-25 12:02:28 +00:00
|
|
|
}, message: {
|
|
|
|
Text(viewModel.postingError ?? "")
|
|
|
|
})
|
2022-12-23 09:41:55 +00:00
|
|
|
.toolbar {
|
|
|
|
ToolbarItem(placement: .navigationBarTrailing) {
|
|
|
|
Button {
|
2022-12-25 07:17:16 +00:00
|
|
|
Task {
|
2023-02-12 17:24:09 +00:00
|
|
|
viewModel.evaluateLanguages()
|
2023-07-17 18:39:53 +00:00
|
|
|
if preferences.autoDetectPostLanguage, let _ = viewModel.languageConfirmationDialogLanguages {
|
2023-02-12 17:24:09 +00:00
|
|
|
isLanguageConfirmPresented = true
|
|
|
|
} else {
|
|
|
|
await postStatus()
|
|
|
|
}
|
2022-12-25 07:17:16 +00:00
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
} label: {
|
2022-12-25 16:46:51 +00:00
|
|
|
if viewModel.isPosting {
|
|
|
|
ProgressView()
|
|
|
|
} else {
|
2023-02-22 21:12:10 +00:00
|
|
|
Text("status.action.post").bold()
|
2022-12-25 16:46:51 +00:00
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
2023-01-10 13:44:11 +00:00
|
|
|
.disabled(!viewModel.canPost)
|
2023-01-18 07:27:42 +00:00
|
|
|
.keyboardShortcut(.return, modifiers: .command)
|
2023-02-12 17:23:29 +00:00
|
|
|
.confirmationDialog("", isPresented: $isLanguageConfirmPresented, actions: {
|
2023-02-12 17:24:09 +00:00
|
|
|
languageConfirmationDialog
|
2023-02-12 17:23:29 +00:00
|
|
|
})
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
|
|
|
ToolbarItem(placement: .navigationBarLeading) {
|
|
|
|
Button {
|
2023-01-22 10:28:23 +00:00
|
|
|
if viewModel.shouldDisplayDismissWarning {
|
2023-01-11 11:44:34 +00:00
|
|
|
isDismissAlertPresented = true
|
|
|
|
} else {
|
|
|
|
dismiss()
|
2023-01-15 15:39:08 +00:00
|
|
|
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
|
|
|
object: nil)
|
2023-01-11 11:44:34 +00:00
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
} label: {
|
2023-01-19 17:14:08 +00:00
|
|
|
Text("action.cancel")
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
2023-01-18 07:27:42 +00:00
|
|
|
.keyboardShortcut(.cancelAction)
|
2023-09-22 07:31:35 +00:00
|
|
|
.confirmationDialog(
|
|
|
|
"",
|
|
|
|
isPresented: $isDismissAlertPresented,
|
|
|
|
actions: {
|
|
|
|
Button("status.draft.delete", role: .destructive) {
|
|
|
|
dismiss()
|
|
|
|
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
|
|
|
object: nil)
|
|
|
|
}
|
|
|
|
Button("status.draft.save") {
|
|
|
|
context.insert(Draft(content: viewModel.statusText.string))
|
|
|
|
dismiss()
|
|
|
|
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
|
|
|
object: nil)
|
|
|
|
}
|
|
|
|
Button("action.cancel", role: .cancel) {}
|
2023-10-01 07:37:09 +00:00
|
|
|
}
|
|
|
|
)
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-10-16 07:16:17 +00:00
|
|
|
.sheet(item: $editingContainer) { container in
|
|
|
|
StatusEditorMediaEditView(viewModel: viewModel, container: container)
|
|
|
|
.preferredColorScheme(theme.selectedScheme == .dark ? .dark : .light)
|
|
|
|
}
|
2023-02-21 17:50:45 +00:00
|
|
|
.interactiveDismissDisabled(viewModel.shouldDisplayDismissWarning)
|
2023-09-18 05:01:23 +00:00
|
|
|
.onChange(of: appAccounts.currentClient) { _, newValue in
|
2023-02-17 07:09:26 +00:00
|
|
|
if viewModel.mode.isInShareExtension {
|
2023-09-18 05:01:23 +00:00
|
|
|
currentAccount.setClient(client: newValue)
|
|
|
|
viewModel.client = newValue
|
2023-02-17 07:09:26 +00:00
|
|
|
}
|
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-02-12 17:24:09 +00:00
|
|
|
@ViewBuilder
|
|
|
|
private var languageConfirmationDialog: some View {
|
2023-03-13 12:38:28 +00:00
|
|
|
if let (detected: detected, selected: selected) = viewModel.languageConfirmationDialogLanguages,
|
2023-02-12 17:24:09 +00:00
|
|
|
let detectedLong = Locale.current.localizedString(forLanguageCode: detected),
|
|
|
|
let selectedLong = Locale.current.localizedString(forLanguageCode: selected)
|
|
|
|
{
|
|
|
|
Button("status.editor.language-select.confirmation.detected-\(detectedLong)") {
|
|
|
|
viewModel.selectedLanguage = detected
|
|
|
|
Task {
|
|
|
|
await postStatus()
|
2023-02-12 17:23:29 +00:00
|
|
|
}
|
2023-02-12 17:24:09 +00:00
|
|
|
}
|
|
|
|
Button("status.editor.language-select.confirmation.selected-\(selectedLong)") {
|
|
|
|
viewModel.selectedLanguage = selected
|
|
|
|
Task {
|
|
|
|
await postStatus()
|
2023-02-12 17:23:29 +00:00
|
|
|
}
|
2023-02-12 17:24:09 +00:00
|
|
|
}
|
|
|
|
Button("action.cancel", role: .cancel) {
|
|
|
|
viewModel.languageConfirmationDialogLanguages = nil
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
EmptyView()
|
2023-02-12 17:23:29 +00:00
|
|
|
}
|
2023-02-12 17:24:09 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private func postStatus() async {
|
|
|
|
let status = await viewModel.postStatus()
|
|
|
|
if status != nil {
|
|
|
|
dismiss()
|
2023-03-01 06:04:07 +00:00
|
|
|
SoundEffectManager.shared.playSound(of: .tootSent)
|
2023-02-12 17:24:09 +00:00
|
|
|
NotificationCenter.default.post(name: NotificationsName.shareSheetClose,
|
|
|
|
object: nil)
|
2023-10-23 17:12:25 +00:00
|
|
|
if !viewModel.mode.isInShareExtension, !preferences.requestedReview, !ProcessInfo.processInfo.isMacCatalystApp {
|
2023-02-16 12:22:04 +00:00
|
|
|
if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene {
|
2023-02-18 06:26:48 +00:00
|
|
|
SKStoreReviewController.requestReview(in: scene)
|
|
|
|
}
|
2023-02-16 11:14:57 +00:00
|
|
|
preferences.requestedReview = true
|
|
|
|
}
|
2023-02-12 17:24:09 +00:00
|
|
|
}
|
|
|
|
}
|
2023-02-12 17:23:29 +00:00
|
|
|
|
2022-12-28 09:45:05 +00:00
|
|
|
@ViewBuilder
|
|
|
|
private var spoilerTextView: some View {
|
|
|
|
if viewModel.spoilerOn {
|
|
|
|
VStack {
|
2023-01-19 17:14:08 +00:00
|
|
|
TextField("status.editor.spoiler", text: $viewModel.spoilerText)
|
2022-12-28 09:45:05 +00:00
|
|
|
.focused($isSpoilerTextFocused)
|
2023-01-03 06:41:29 +00:00
|
|
|
.padding(.horizontal, .layoutPadding)
|
2022-12-28 09:45:05 +00:00
|
|
|
}
|
|
|
|
.frame(height: 35)
|
2022-12-29 09:39:34 +00:00
|
|
|
.background(theme.tintColor.opacity(0.20))
|
2022-12-28 09:45:05 +00:00
|
|
|
.offset(y: -8)
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2022-12-25 18:15:35 +00:00
|
|
|
@ViewBuilder
|
|
|
|
private var accountHeaderView: some View {
|
2023-02-15 05:44:51 +00:00
|
|
|
if let account = currentAccount.account, !viewModel.mode.isEditing {
|
2022-12-25 05:55:33 +00:00
|
|
|
HStack {
|
2023-02-17 13:02:05 +00:00
|
|
|
if viewModel.mode.isInShareExtension {
|
|
|
|
AppAccountsSelectorView(routerPath: RouterPath(),
|
|
|
|
accountCreationEnabled: false,
|
|
|
|
avatarSize: .status)
|
|
|
|
} else {
|
|
|
|
AvatarView(url: account.avatar, size: .status)
|
2023-09-18 19:03:52 +00:00
|
|
|
.environment(theme)
|
Compose Post Screen Accessibility Tweaks (#1259)
* Add localized label for the AI prompt status accessory view
Previously, this icon would have an accessibility label matching its SF symbol key, ‘faxmachine’.
* Darken status editor character count foreground color
By changing it to .secondary, it gets to an APCA contrast of 61, which is a _just_ passing Bronze score for that text size.
It’s still quite short of WCAG 2.1 AA at 3.3:1 (recommended is 4.5:1)
* Change remaining character count color to red when < 0
* Refine remaining character count accessibility
In this commit, we
- Change its trait to `.updatesFrequently`
- Set a localized `accessibilityLabel`
- Set its `accessibilityValue` to the remaining character count
- Disable user interaction (which is presumably set automatically by virtue of being enclosed in a `Menu`)
* Set accessibilitySortPriority on Status editor ScrollView
Previously, the traversal order placed the elements inside the `ScrollView` last. Now, they follow on from the navigation bar contents in the expected order.
* Hide the AvatarView from status creation accessibility
When there is only one account available, there is no functionality associated with this element, so it is considered decorative-only, and should be hidden
* Set TextView placeholder’s `accessibilityValue` to placeholder text when empty
This behaviour matches `UITextField`
* Hide TextView custom `placeholderView` from accessibility
Previously, TextView would vend two accessibility elements when the placeholder was visible. This causes needless confusion for users.
Now, the TextView matches the accessible behaviour of text inputs elsewhere.
* Improve accessibility of post `privacyMenu`
Previously, it would be presented as `Everyone, Button`. Now, we move the visibility to its `value` and use `Visibility` for its label, in conjunction with a hint that states it `Changes post audience`.
* Add `.button` trait and accessible label to emojis in `customEmojisSheet`
Previously, these would all present as `image` with no description, making it very hard to discern what kind of emoji you were adding.
* Change drafts sheet item type to `Button`
A button with an action has a more accessible representation than a `Text` with a tap gesture.
2023-03-17 05:39:31 +00:00
|
|
|
.accessibilityHidden(true)
|
2023-02-17 13:02:05 +00:00
|
|
|
}
|
2023-01-11 11:44:34 +00:00
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
|
|
privacyMenu
|
2023-02-17 13:02:05 +00:00
|
|
|
Text("@\(account.acct)@\(appAccounts.currentClient.server)")
|
2023-01-17 18:41:46 +00:00
|
|
|
.font(.scaledFootnote)
|
2022-12-25 05:55:33 +00:00
|
|
|
.foregroundColor(.gray)
|
|
|
|
}
|
|
|
|
Spacer()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-01-17 10:36:01 +00:00
|
|
|
|
2023-01-11 11:44:34 +00:00
|
|
|
private var privacyMenu: some View {
|
|
|
|
Menu {
|
2023-01-19 17:14:08 +00:00
|
|
|
Section("status.editor.visibility") {
|
2023-01-11 11:44:34 +00:00
|
|
|
ForEach(Models.Visibility.allCases, id: \.self) { visibility in
|
|
|
|
Button {
|
|
|
|
viewModel.visibility = visibility
|
|
|
|
} label: {
|
|
|
|
Label(visibility.title, systemImage: visibility.iconName)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} label: {
|
|
|
|
HStack {
|
|
|
|
Label(viewModel.visibility.title, systemImage: viewModel.visibility.iconName)
|
Compose Post Screen Accessibility Tweaks (#1259)
* Add localized label for the AI prompt status accessory view
Previously, this icon would have an accessibility label matching its SF symbol key, ‘faxmachine’.
* Darken status editor character count foreground color
By changing it to .secondary, it gets to an APCA contrast of 61, which is a _just_ passing Bronze score for that text size.
It’s still quite short of WCAG 2.1 AA at 3.3:1 (recommended is 4.5:1)
* Change remaining character count color to red when < 0
* Refine remaining character count accessibility
In this commit, we
- Change its trait to `.updatesFrequently`
- Set a localized `accessibilityLabel`
- Set its `accessibilityValue` to the remaining character count
- Disable user interaction (which is presumably set automatically by virtue of being enclosed in a `Menu`)
* Set accessibilitySortPriority on Status editor ScrollView
Previously, the traversal order placed the elements inside the `ScrollView` last. Now, they follow on from the navigation bar contents in the expected order.
* Hide the AvatarView from status creation accessibility
When there is only one account available, there is no functionality associated with this element, so it is considered decorative-only, and should be hidden
* Set TextView placeholder’s `accessibilityValue` to placeholder text when empty
This behaviour matches `UITextField`
* Hide TextView custom `placeholderView` from accessibility
Previously, TextView would vend two accessibility elements when the placeholder was visible. This causes needless confusion for users.
Now, the TextView matches the accessible behaviour of text inputs elsewhere.
* Improve accessibility of post `privacyMenu`
Previously, it would be presented as `Everyone, Button`. Now, we move the visibility to its `value` and use `Visibility` for its label, in conjunction with a hint that states it `Changes post audience`.
* Add `.button` trait and accessible label to emojis in `customEmojisSheet`
Previously, these would all present as `image` with no description, making it very hard to discern what kind of emoji you were adding.
* Change drafts sheet item type to `Button`
A button with an action has a more accessible representation than a `Text` with a tap gesture.
2023-03-17 05:39:31 +00:00
|
|
|
.accessibilityLabel("accessibility.editor.privacy.label")
|
|
|
|
.accessibilityValue(viewModel.visibility.title)
|
|
|
|
.accessibilityHint("accessibility.editor.privacy.hint")
|
2023-01-11 11:44:34 +00:00
|
|
|
Image(systemName: "chevron.down")
|
|
|
|
}
|
2023-01-17 18:41:46 +00:00
|
|
|
.font(.scaledFootnote)
|
2023-01-11 11:44:34 +00:00
|
|
|
.padding(4)
|
|
|
|
.overlay(
|
|
|
|
RoundedRectangle(cornerRadius: 8)
|
|
|
|
.stroke(theme.tintColor, lineWidth: 1)
|
|
|
|
)
|
|
|
|
}
|
|
|
|
}
|
2022-12-23 09:41:55 +00:00
|
|
|
}
|