diff --git a/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift b/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift index 85d8ea78..4ee7cab9 100644 --- a/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift +++ b/Modules/UtilsLib/Sources/UtilsLib/Extensions/URLExtensions.swift @@ -398,3 +398,12 @@ extension URL { } } } + +extension URL { + public var isPlainText: Bool { + guard let type = UTType( + filenameExtension: pathExtension.lowercased() + ) else { return false } + return type.conforms(to: .text) + } +} diff --git a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift index 18676618..235a83bb 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/CryptoDataFilesSection.swift @@ -83,7 +83,7 @@ struct CryptoDataFilesSection: View { } ) .background(fileSaverBackground) - .quickLookPreview($viewModel.previewFile) + .filePreview(item: $viewModel.previewFile) } private func openFile(_ dataFile: URL) { diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index 84f70b8f..f1d183ab 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -18,7 +18,6 @@ */ import SwiftUI -import QuickLook import FactoryKit import CryptoObjCWrapper import CommonsLib @@ -310,7 +309,7 @@ struct EncryptView: View { isFileSaved: $isFileSaved ) ) - .quickLookPreview($viewModel.previewFile) + .filePreview(item: $viewModel.previewFile) } .padding(.vertical, Dimensions.Padding.MPadding) } else { diff --git a/RIADigiDoc/UI/Component/Container/DataFilesSection.swift b/RIADigiDoc/UI/Component/Container/DataFilesSection.swift index ddfab5bd..42d2f8fd 100644 --- a/RIADigiDoc/UI/Component/Container/DataFilesSection.swift +++ b/RIADigiDoc/UI/Component/Container/DataFilesSection.swift @@ -76,7 +76,7 @@ struct DataFilesSection: View { ) .alert(sivaMessage, isPresented: $showSivaMessage, actions: alertActions) .background(fileSaverBackground) - .quickLookPreview($viewModel.previewFile) + .filePreview(item: $viewModel.previewFile) } private func openFile(_ dataFile: DataFileWrapper) { diff --git a/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift b/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift index 53f75cb1..a5349719 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/ControlCodeView.swift @@ -31,6 +31,12 @@ struct ControlCodeView: View { @Binding var controlCode: String @Binding var infoMessage: String + @AccessibilityFocusState private var isControlCodeFocused: Bool + + private var isControlCodeValid: Bool { + !controlCode.isEmpty && controlCode.allSatisfy { $0.isNumber } + } + var body: some View { VStack(alignment: .center) { Image(icon) @@ -46,18 +52,30 @@ struct ControlCodeView: View { Text(verbatim: languageSettings.localized("Control code")) .font(typography.bodyLarge) .foregroundStyle(theme.onSurface) + .accessibilityHidden(!isControlCodeValid) Text(verbatim: controlCode) + .speechSpellsOutCharacters(true) .font(typography.displayMedium) .foregroundStyle(theme.onSurface) .scaleEffect(x: Dimensions.Scaling.WideScaling, y: Dimensions.Scaling.DefaultScaling) .accessibilityIdentifier("controlCode") + .accessibilityHidden(!isControlCodeValid) Text(verbatim: languageSettings.localized(infoMessage)) .font(typography.bodyLarge) .foregroundStyle(theme.onSurface) .accessibilityIdentifier("infoMessage") } + .onChange(of: controlCode) { _, newValue in + if (!newValue.isEmpty && newValue.allSatisfy { $0.isNumber }) { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + isControlCodeFocused = true + } + } + } + .accessibilityFocused($isControlCodeFocused) + .accessibilityElement(children: .combine) } .onDisappear { controlCode = "- - - -" diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index 05ac1acb..185b19a1 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -25,6 +25,7 @@ import IdCardLib import CommonsLib struct IdCardView: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.openURL) private var openURL @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @@ -116,6 +117,10 @@ struct IdCardView: View { viewModel.idCardAlertMessageKey?.isEmpty == false } + private var signatureAddedMessage: String { + languageSettings.localized("Signature added") + } + init( actionType: ActionType, actionMethods: [ActionMethod], @@ -487,10 +492,10 @@ struct IdCardView: View { isShowingPinView = false isShowingLoadingView = false - Toast.show( - languageSettings.localized("Signature added"), - type: .success - ) + Toast.show(signatureAddedMessage, type: .success) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(signatureAddedMessage) + } onSuccess(container) dismiss() diff --git a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift index 8a76f7c9..189aeb97 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift @@ -23,6 +23,7 @@ import LibdigidocLibSwift import CommonsLib struct MobileIdView: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.openURL) private var openURL @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @@ -138,9 +139,13 @@ struct MobileIdView: View { if let messageKey = newValue, !messageKey.isEmpty { let extraArguments = viewModel.mobileIdAlertMessageExtraArguments - Toast.show( - languageSettings.localized(messageKey, extraArguments) - ) + let message = languageSettings.localized(messageKey, extraArguments) + + Toast.show(message) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(message) + } + viewModel.mobileIdErrorMessageKey = nil viewModel.mobileIdAlertMessageExtraArguments = [] } diff --git a/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift b/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift index 16e8b938..deb70faa 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/Modal/ConfirmModalView.swift @@ -38,6 +38,7 @@ struct ConfirmModalView: View { .opacity(Dimensions.Shadow.LOpacity) .ignoresSafeArea() .accessibilityHidden(true) + .allowsHitTesting(true) TextModal( title: title, @@ -54,5 +55,6 @@ struct ConfirmModalView: View { .accessibilityAddTraits(.isModal) .accessibilityElement(children: .contain) } + .accessibilityAddTraits(.isModal) } } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift index 115d0ebb..88648a51 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCInputView.swift @@ -87,7 +87,8 @@ struct NFCInputView: View { text: $canNumber, isError: !(canNumberError?.isEmpty ?? true), errorText: canNumberError ?? "", - keyboardType: .numberPad + keyboardType: .numberPad, + sortPriority: 0 ) .onChange(of: canNumber) { onInputChange() @@ -96,10 +97,12 @@ struct NFCInputView: View { Text(verbatim: canNumberLocationLabel) .font(typography.labelMedium) .foregroundStyle(theme.onSecondaryContainer) - .padding(.vertical, Dimensions.Padding.XXSPadding) + .padding(.top, Dimensions.Padding.XXSPadding) + .accessibilitySortPriority(1) } + .accessibilityElement(children: .contain) } - .padding(.vertical, Dimensions.Padding.ZeroPadding) + .padding(.bottom, Dimensions.Padding.MPadding) if showPinField { VStack(alignment: .leading, spacing: Dimensions.Padding.ZeroPadding) { diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index 67f35e49..b97170fa 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -23,7 +23,9 @@ import CryptoSwift import IdCardLib import LibdigidocLibSwift import CommonsLib + struct NFCView: View { + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.dismiss) private var dismiss @Environment(\.openURL) private var openURL @Environment(LanguageSettings.self) private var languageSettings @@ -95,6 +97,10 @@ struct NFCView: View { languageSettings.localized(key, args) } } + + private var signatureAddedMessage: String { + languageSettings.localized("Signature added") + } let signedContainer: SignedContainerProtocol? let cryptoContainer: CryptoContainerProtocol? @@ -358,6 +364,8 @@ struct NFCView: View { guard let container = updatedContainer else { return } + + Toast.show(signatureAddedMessage, type: .success) onSuccess(container) dismiss() diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift index a88f1eaf..17f6c11c 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningView.swift @@ -18,7 +18,6 @@ */ import SwiftUI -import QuickLook import FactoryKit import LibdigidocLibSwift import CommonsLib @@ -317,7 +316,7 @@ struct SigningView: View { isFileSaved: $isFileSaved ) ) - .quickLookPreview($viewModel.previewFile) + .filePreview(item: $viewModel.previewFile) } .padding(.vertical, Dimensions.Padding.MPadding) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift index 0d943d15..21e678cd 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift @@ -24,6 +24,7 @@ import CommonsLib struct SmartIdView: View { @Environment(\.scenePhase) private var scenePhase + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.openURL) private var openURL @Environment(\.dismiss) private var dismiss @Environment(LanguageSettings.self) private var languageSettings @@ -149,9 +150,12 @@ struct SmartIdView: View { .onChange(of: viewModel.smartIdErrorMessageKey, { _, newValue in if let messageKey = newValue, !messageKey.isEmpty { let extraArguments = viewModel.smartIdAlertMessageExtraArguments - Toast.show( - languageSettings.localized(messageKey, extraArguments) - ) + let message = languageSettings.localized(messageKey, extraArguments) + Toast.show(message) + if voiceOverEnabled { + AccessibilityUtil.announceMessage(message) + } + viewModel.smartIdErrorMessageKey = nil viewModel.smartIdAlertMessageExtraArguments = [] } diff --git a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift index cf76abfc..1f3857d5 100644 --- a/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift +++ b/RIADigiDoc/UI/Component/Container/UnsignedBottomBarView.swift @@ -52,6 +52,7 @@ struct UnsignedBottomBarView: View { Text(languageSettings.localized(leftButtonLabel)) .foregroundStyle(theme.primary) .font(typography.titleMedium) + .minimumScaleFactor(0.5) .accessibilityLabel(leftButtonAccessibilityLabel) }) .foregroundStyle(theme.surfaceContainer) @@ -72,6 +73,7 @@ struct UnsignedBottomBarView: View { Text(languageSettings.localized(rightButtonLabel)) .foregroundStyle(theme.primary) .font(typography.titleMedium) + .minimumScaleFactor(0.5) .accessibilityLabel(rightButtonAccessibilityLabel) } .padding(.horizontal, Dimensions.Padding.MPadding) diff --git a/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift b/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift index e300d53c..d1f8f068 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidPinChangeView.swift @@ -259,6 +259,7 @@ struct MyEidPinChangeView: View { .frame(maxWidth: .infinity, alignment: .leading) .accessibilitySortPriority(1) } + .accessibilityElement(children: .contain) } } diff --git a/RIADigiDoc/UI/Component/Shared/Extension/ViewExtensions.swift b/RIADigiDoc/UI/Component/Shared/Extension/ViewExtensions.swift index 1ac97c73..1ba71711 100644 --- a/RIADigiDoc/UI/Component/Shared/Extension/ViewExtensions.swift +++ b/RIADigiDoc/UI/Component/Shared/Extension/ViewExtensions.swift @@ -56,3 +56,27 @@ extension View { } } } + +extension View { + func filePreview(item: Binding) -> some View { + self.sheet(item: item) { file in + FilePreviewSheet(url: file.url, isPresented: Binding( + get: { item.wrappedValue != nil }, + set: { if !$0 { item.wrappedValue = nil } } + )) + } + } +} + +private struct FilePreviewSheet: View { + let url: URL + @Binding var isPresented: Bool + + var body: some View { + if url.isPlainText { + TextFilePreview(url: url, isPresented: $isPresented) + } else { + PreviewController(url: url, isPresented: $isPresented) + } + } +} diff --git a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift index 3f916ea9..dd41b70a 100644 --- a/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift +++ b/RIADigiDoc/UI/Component/Shared/FloatingLabelTextField.swift @@ -29,6 +29,8 @@ struct FloatingLabelTextField: View { @AccessibilityFocusState private var isAccessibilityFocused: Bool + @State private var selection: TextSelection? = nil + @State private var floatingLabelHeight: CGFloat = 0 // MARK: - Parameters @@ -257,8 +259,6 @@ struct FloatingLabelTextField: View { private var containerContent: some View { HStack(spacing: Dimensions.Padding.XSPadding) { inputField - .accessibilityLabel(Text(verbatim: textFieldAccessibility)) - .accessibilityValue(Text(verbatim: "")) Spacer() trailingIcon } @@ -290,48 +290,26 @@ struct FloatingLabelTextField: View { prompt: Text(verbatim: placeholder) .foregroundStyle(theme.onSurfaceVariant) ) - .multilineTextAlignment(.leading) - .disabled(isDisabled) - .keyboardType(keyboardType) - .submitLabel(submitLabel) - .textContentType(.none) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - .privacySensitive() - .speechSpellsOutCharacters(spellOutCharacters && isPasswordVisible) - .accessibilityFocused($isAccessibilityFocused) - .onSubmit { - isFocused = false - isAccessibilityFocused = true - onDone() - } - .toolbar { - ToolbarItem(placement: .keyboard) { - if fieldIsFocused { - HStack { - if showDashButton { - Button( - action: { text.append("-") }, - label: { Text(verbatim: "-") } - ) - } - - if keyboardType.needsDoneButton { - Button( - action: { - fieldIsFocused = false - isAccessibilityFocused = true - onDone() - }, - label: { Text(verbatim: languageSettings.localized("Done")) } - ) - } - } - } + .textFieldModifiers( + isDisabled: isDisabled, + keyboardType: keyboardType, + submitLabel: submitLabel, + spellOut: spellOutCharacters && isPasswordVisible, + isAccessibilityFocused: $isAccessibilityFocused, + onAppear: {}, + onSubmit: { + isFocused = false + isAccessibilityFocused = true + onDone() } - } - .accessibilityValue(Text(verbatim: "")) + ) + .privacySensitive() + .toolbar { keyboardToolbar } + .onChange(of: errorText, { _, newValue in + AccessibilityUtil.announceMessage(newValue) + }) .accessibilitySortPriority(sortPriority) + .accessibilityLabel(Text(verbatim: title)) } else { TextField( placeholder, @@ -339,47 +317,24 @@ struct FloatingLabelTextField: View { prompt: Text(verbatim: placeholder) .foregroundStyle(theme.onSurfaceVariant) ) - .multilineTextAlignment(.leading) - .disabled(isDisabled) - .keyboardType(keyboardType) - .submitLabel(submitLabel) - .textContentType(.none) - .autocorrectionDisabled(true) - .textInputAutocapitalization(.never) - .speechSpellsOutCharacters(spellOutCharacters && !isSecure && isPasswordVisible) - .accessibilityFocused($isAccessibilityFocused) - .onSubmit { - fieldIsFocused = false - isAccessibilityFocused = true - onDone() - } - .toolbar { - ToolbarItem(placement: .keyboard) { - if fieldIsFocused { - HStack { - if showDashButton { - Button( - action: { text.append("-") }, - label: { Text(verbatim: "-") } - ) - } - - if keyboardType.needsDoneButton { - Button( - action: { - fieldIsFocused = false - isAccessibilityFocused = true - onDone() - }, - label: { Text(verbatim: languageSettings.localized("Done")) } - ) - } - } - } + .textFieldModifiers( + isDisabled: isDisabled, + keyboardType: keyboardType, + submitLabel: submitLabel, + spellOut: spellOutCharacters && !isSecure && isPasswordVisible, + isAccessibilityFocused: $isAccessibilityFocused, + onAppear: { + selection = TextSelection(insertionPoint: text.endIndex) + }, + onSubmit: { + fieldIsFocused = false + isAccessibilityFocused = true + onDone() } - } - .accessibilityValue(Text(verbatim: "")) + ) + .toolbar { keyboardToolbar } .accessibilitySortPriority(sortPriority) + .accessibilityLabel(Text(verbatim: title)) } } .font(typography.bodyLarge) @@ -396,7 +351,33 @@ struct FloatingLabelTextField: View { AccessibilityUtil.announceMessage(newValue) }) .frame(height: Dimensions.Icon.IconSizeXXS) - .accessibilityValue(Text(verbatim: "")) + } + + @ToolbarContentBuilder + private var keyboardToolbar: some ToolbarContent { + ToolbarItem(placement: .keyboard) { + if fieldIsFocused { + HStack { + if showDashButton { + Button( + action: { text.append("-") }, + label: { Text(verbatim: "-") } + ) + } + + if keyboardType.needsDoneButton { + Button( + action: { + fieldIsFocused = false + isAccessibilityFocused = true + onDone() + }, + label: { Text(verbatim: languageSettings.localized("Done")) } + ) + } + } + } + } } // MARK: - Icons @@ -509,6 +490,31 @@ struct FloatingLabelTextField: View { } } +private extension View { + func textFieldModifiers( + isDisabled: Bool, + keyboardType: UIKeyboardType, + submitLabel: SubmitLabel, + spellOut: Bool, + isAccessibilityFocused: AccessibilityFocusState.Binding, + onAppear: @escaping () -> Void, + onSubmit: @escaping () -> Void + ) -> some View { + self + .multilineTextAlignment(.leading) + .disabled(isDisabled) + .keyboardType(keyboardType) + .submitLabel(submitLabel) + .textContentType(.none) + .autocorrectionDisabled(true) + .textInputAutocapitalization(.never) + .speechSpellsOutCharacters(spellOut) + .accessibilityFocused(isAccessibilityFocused) + .onAppear(perform: onAppear) + .onSubmit(onSubmit) + } +} + // MARK: - Preview #Preview { diff --git a/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift b/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift index f656dac3..2c8db589 100644 --- a/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift +++ b/RIADigiDoc/UI/Component/Shared/Modal/ModalContainer.swift @@ -49,6 +49,12 @@ struct ModalContainer: View { .accessibilityHeading(.h1) .accessibilityAddTraits([.isHeader]) .accessibilityFocused($isFocused) + .onAppear { + Task { + try? await Task.sleep(for: .seconds(0.3)) + isFocused = true + } + } } private var containerContent: some View { @@ -112,12 +118,5 @@ struct ModalContainer: View { ) .padding(.horizontal, Dimensions.Padding.XLPadding) .accessibilityAddTraits([.isModal]) - .onAppear { - Task { - await MainActor.run { - isFocused = true - } - } - } } } diff --git a/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift b/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift new file mode 100644 index 00000000..e4b650f9 --- /dev/null +++ b/RIADigiDoc/UI/Component/Shared/Preview/PreviewController.swift @@ -0,0 +1,111 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI +import QuickLook + +struct PreviewController: UIViewControllerRepresentable { + let url: URL + @Binding var isPresented: Bool + + func makeUIViewController(context: Context) -> UINavigationController { + let controller = QLPreviewController() + controller.dataSource = context.coordinator + + let dismissAction = UIAction( + title: "Done", + handler: { [weak coordinator = context.coordinator] _ in + coordinator?.dismiss() + } + ) + let doneButton = UIBarButtonItem(primaryAction: dismissAction) + controller.navigationItem.leftBarButtonItem = doneButton + + let navController = UINavigationController(rootViewController: controller) + + let escCommand = UIKeyCommand( + input: UIKeyCommand.inputEscape, + modifierFlags: [], + action: #selector(Coordinator.dismiss) + ) + + escCommand.wantsPriorityOverSystemBehavior = true + + navController.addKeyCommand(escCommand) + + return navController + } + + func updateUIViewController(_ uiViewController: UINavigationController, context: Context) {} + + func makeCoordinator() -> Coordinator { + Coordinator(isPresented: $isPresented, url: url) + } + + class Coordinator: NSObject, QLPreviewControllerDataSource { + @Binding var isPresented: Bool + let url: URL + + init(isPresented: Binding, url: URL) { + self._isPresented = isPresented + self.url = url + } + + @objc func dismiss() { + isPresented = false + } + + func numberOfPreviewItems(in controller: QLPreviewController) -> Int { 1 } + + func previewController( + _ controller: QLPreviewController, + previewItemAt index: Int + ) -> QLPreviewItem { + url as NSURL + } + } +} + +class PreviewQLController: QLPreviewController { + var onEscape: (() -> Void)? + + override var canBecomeFirstResponder: Bool { true } + + override func viewDidAppear(_ animated: Bool) { + super.viewDidAppear(animated) + + addKeyCommand(UIKeyCommand( + input: UIKeyCommand.inputEscape, + modifierFlags: [], + action: #selector(handleEscape) + )) + } + + @objc private func handleEscape() { + onEscape?() + } + + override func pressesBegan(_ presses: Set, with event: UIPressesEvent?) { + if presses.contains(where: { $0.key?.keyCode == .keyboardEscape }) { + onEscape?() + return + } + super.pressesBegan(presses, with: event) + } +} diff --git a/RIADigiDoc/UI/Component/Shared/Preview/TextFilePreview.swift b/RIADigiDoc/UI/Component/Shared/Preview/TextFilePreview.swift new file mode 100644 index 00000000..55d12cd5 --- /dev/null +++ b/RIADigiDoc/UI/Component/Shared/Preview/TextFilePreview.swift @@ -0,0 +1,95 @@ +/* + * Copyright 2017 - 2026 Riigi Infosüsteemi Amet + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + */ + +import SwiftUI + +struct TextFilePreview: View { + @Environment(LanguageSettings.self) private var languageSettings + + @AppTheme private var theme + @AppTypography private var typography + + let url: URL + @Binding var isPresented: Bool + @State private var text: String = "" + @State private var showError: Bool = false + + var body: some View { + ZStack(alignment: .topTrailing) { + Group { + if showError { + ContentUnavailableView { + Text( + verbatim: languageSettings.localized( + "Failed to open file", [url.lastPathComponent] + ) + ) + } + .listRowSeparator(.hidden) + } else { + ScrollView { + Text(verbatim: text) + .font(typography.bodyMedium) + .foregroundStyle(theme.onSurfaceVariant) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, Dimensions.Padding.SPadding) + .padding(.vertical, Dimensions.Padding.XXLPadding) + .textSelection(.enabled) + } + } + } + + ZStack { + Text(verbatim: url.lastPathComponent) + .font(typography.bodyMedium) + .foregroundStyle(theme.onSurfaceVariant) + .frame(maxWidth: .infinity, alignment: .center) + + HStack { + Spacer() + Button { + isPresented = false + } label: { + Image("ic_m3_close_48pt_wght400") + .resizable() + .scaledToFit() + .frame( + width: Dimensions.Icon.IconSizeXXS, + height: Dimensions.Icon.IconSizeXXS + ) + .padding(.horizontal, Dimensions.Padding.MPadding) + .padding(.vertical, Dimensions.Padding.MSPadding) + .foregroundStyle(theme.onSurface) + .accessibilityLabel(languageSettings.localized("Close")) + } + .keyboardShortcut(.escape, modifiers: []) + .padding(Dimensions.Padding.XSPadding) + } + } + } + .task { + do { + var detectedEncoding: String.Encoding = .utf8 + text = try String(contentsOfFile: url.path, usedEncoding: &detectedEncoding) + } catch { + self.showError = true + } + } + } +} diff --git a/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift b/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift index a05b1976..ddbef74d 100644 --- a/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift +++ b/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift @@ -222,6 +222,7 @@ struct TopBar: View { .foregroundStyle(theme.onSurface) .font(typography.titleLarge) .padding(.leading, Dimensions.Padding.XSPadding) + .minimumScaleFactor(0.5) .accessibilityLabel(titleAccessibility ?? title) .accessibilityAddTraits(.isHeader) } diff --git a/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift b/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift index ccb1629d..641cd004 100644 --- a/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift +++ b/RIADigiDoc/UI/Component/Toast/ToastOverlay.swift @@ -55,6 +55,7 @@ struct ToastOverlay: View { height: Dimensions.Icon.IconSizeXXS ) .foregroundStyle(style.foreground) + .accessibilityHidden(true) Text(verbatim: message) .lineLimit(nil) diff --git a/RIADigiDoc/ViewModel/EncryptViewModel.swift b/RIADigiDoc/ViewModel/EncryptViewModel.swift index 1f1c8e68..6b9486c7 100644 --- a/RIADigiDoc/ViewModel/EncryptViewModel.swift +++ b/RIADigiDoc/ViewModel/EncryptViewModel.swift @@ -34,7 +34,7 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { var containerName: String = CommonsLib.Constants.Container.DefaultName var containerMimetype: String = "N/A" var containerURL: URL? - var previewFile: URL? + var previewFile: FileItem? var selectedDataFile: URL? var isShowingContainerFileSaver = false var isShowingFileSaver = false @@ -360,7 +360,11 @@ class EncryptViewModel: EncryptViewModelProtocol, Loggable { return } } else { - previewFile = fileURL + previewFile = FileItem( + name: fileURL.lastPathComponent, + url: fileURL, + lastOpened: Date.now + ) } case .failure: errorMessage = ToastMessage(key: "Failed to open file", args: [dataFile.lastPathComponent]) diff --git a/RIADigiDoc/ViewModel/SigningViewModel.swift b/RIADigiDoc/ViewModel/SigningViewModel.swift index e2568e80..f9f1347e 100644 --- a/RIADigiDoc/ViewModel/SigningViewModel.swift +++ b/RIADigiDoc/ViewModel/SigningViewModel.swift @@ -34,7 +34,7 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { var containerName: String = CommonsLib.Constants.Container.DefaultName var containerMimetype: String = "N/A" var containerURL: URL? - var previewFile: URL? + var previewFile: FileItem? var selectedDataFile: URL? var isShowingContainerFileSaver = false var isShowingFileSaver = false @@ -418,7 +418,11 @@ class SigningViewModel: SigningViewModelProtocol, Loggable { return } } else { - previewFile = fileURL + previewFile = FileItem( + name: fileURL.lastPathComponent, + url: fileURL, + lastOpened: Date.now + ) } case .failure: errorMessage = ToastMessage(key: "Failed to open file", args: [dataFile.fileName]) diff --git a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift index 07fc6fd8..c6a2de2b 100644 --- a/RIADigiDocTests/ViewModel/SigningViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/SigningViewModelTests.swift @@ -367,7 +367,7 @@ struct SigningViewModelTests: Loggable { await viewModel.handleFileOpening(dataFile: testDataFile, isSivaConfirmed: true) - #expect(viewModel.previewFile?.lastPathComponent == "test.txt") + #expect(viewModel.previewFile?.url.lastPathComponent == "test.txt") #expect(viewModel.errorMessage == nil) }