diff --git a/RIADigiDoc.xcodeproj/project.pbxproj b/RIADigiDoc.xcodeproj/project.pbxproj index c8d24640..842bddce 100644 --- a/RIADigiDoc.xcodeproj/project.pbxproj +++ b/RIADigiDoc.xcodeproj/project.pbxproj @@ -204,7 +204,6 @@ Util/Theme/ThemeSettingsProtocol.swift, ViewModel/AdvancedSettingsViewModel.swift, ViewModel/CertificateDetailViewModel.swift, - ViewModel/ContentViewModel.swift, ViewModel/Crypto/DecryptRootViewModel.swift, ViewModel/Crypto/DecryptRootViewModelProtocol.swift, ViewModel/CryptoFileOpeningViewModel.swift, @@ -226,7 +225,6 @@ ViewModel/MyEid/Shared/SharedMyEidSessionProtocol.swift, ViewModel/Protocols/AdvancedSettingsViewModelProtocol.swift, ViewModel/Protocols/CertificateDetailViewModelProtocol.swift, - ViewModel/Protocols/ContentViewModelProtocol.swift, ViewModel/Protocols/CryptoFileOpeningViewModelProtocol.swift, ViewModel/Protocols/CryptoHomeViewModelProtocol.swift, ViewModel/Protocols/DataFilesViewModelProtocol.swift, diff --git a/RIADigiDoc/ContentView.swift b/RIADigiDoc/ContentView.swift index 0c1a30a4..89f7f646 100644 --- a/RIADigiDoc/ContentView.swift +++ b/RIADigiDoc/ContentView.swift @@ -19,92 +19,10 @@ import FactoryKit import SwiftUI -import UtilsLib struct ContentView: View { - @Environment(\.scenePhase) private var scenePhase - @AppTheme private var theme - @AppTypography private var typography - @Environment(LanguageSettings.self) private var languageSettings - - @Environment(NavigationPathManager.self) private var pathManager - - @State private var viewModel: ContentViewModel - - @State private var openedUrls: [URL] = [] - @State private var showHomeMenuBottomSheetFromButton = false - @State private var showSettingsBottomSheetFromButton = false - - @State private var navigateToAccessibility = false - @State private var navigateToInfo = false - @State private var navigateToDiagnostics = false - - @State private var sharedFilesLoadingTask: Task? - - private var homeMenuBottomSheetActions: [BottomSheetButton] { - HomeMenuBottomSheetActions.actions( - onInfoClick: { - pathManager.navigate(to: .infoView) - }, - onAccessibilityClick: { - pathManager.navigate(to: .accessibilityView) - }, - onDiagnosticsClick: { - pathManager.navigate(to: .diagnosticsView) - } - ) - } - - init() { - _viewModel = State(wrappedValue: Container.shared.contentViewModel()) - } - var body: some View { - TopBarContainer( - leftIcon: "ic_m3_menu_48pt_wght400", - leftIconAccessibility: "Menu", - onLeftClick: { - showHomeMenuBottomSheetFromButton = true - }, - content: { - ScrollView { - VStack { - HomeView(externalFiles: $openedUrls) - Spacer() - } - } - .background(theme.surface) - .onOpenURL { url in - openedUrls = [url] - } - .onAppear { - if scenePhase == .active { - sharedFilesLoadingTask = Task { - let sharedFiles = await viewModel.getSharedFiles() - if !sharedFiles.isEmpty { - openedUrls = sharedFiles - } - } - } - } - .onChange(of: scenePhase) { _, newPhase in - if newPhase == .active { - sharedFilesLoadingTask?.cancel() - - sharedFilesLoadingTask = Task { - let sharedFiles = await viewModel.getSharedFiles() - if !sharedFiles.isEmpty { - openedUrls = sharedFiles - } - } - } - } - .onDisappear { - sharedFilesLoadingTask?.cancel() - } - } - ) - .bottomSheet(isPresented: $showHomeMenuBottomSheetFromButton, actions: homeMenuBottomSheetActions) + HomeView() } } diff --git a/RIADigiDoc/DI/AppContainer.swift b/RIADigiDoc/DI/AppContainer.swift index c62da6d6..0a8a2355 100644 --- a/RIADigiDoc/DI/AppContainer.swift +++ b/RIADigiDoc/DI/AppContainer.swift @@ -88,7 +88,8 @@ extension Container { self { @MainActor in HomeViewModel( sharedContainerViewModel: self.sharedContainerViewModel(), - fileManager: self.fileManager() + fileManager: self.fileManager(), + fileUtil: self.fileUtil() ) } } @@ -207,17 +208,6 @@ extension Container { } } - @MainActor - var contentViewModel: Factory { - self { - @MainActor in - ContentViewModel( - fileUtil: self.fileUtil(), - fileManager: self.fileManager() - ) - } - } - @MainActor var recentDocumentsViewModel: Factory { self { diff --git a/RIADigiDoc/Supporting files/Localizable.xcstrings b/RIADigiDoc/Supporting files/Localizable.xcstrings index b4d5b20f..57e7821f 100644 --- a/RIADigiDoc/Supporting files/Localizable.xcstrings +++ b/RIADigiDoc/Supporting files/Localizable.xcstrings @@ -4424,74 +4424,74 @@ } } }, - "Menu language" : { - "comment" : "Menu language", + "Menu language selected" : { + "comment" : "Menu language selected", "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Language" + "value" : "selected" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Keel" + "value" : "valitud" } } } }, - "Menu language selected" : { - "comment" : "Menu language selected", + "Menu language unselected" : { + "comment" : "Menu language", "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Language selected" + "value" : "unselected" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Keel valitud" + "value" : "valimata" } } } }, - "Menu theme" : { - "comment" : "Menu theme for accessibility", + "Menu theme selected" : { + "comment" : "Menu theme selected for accessibility", "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Theme" + "value" : "Theme selected" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Teema" + "value" : "Teema valitud" } } } }, - "Menu theme selected" : { - "comment" : "Menu theme selected for accessibility", + "Menu theme unselected" : { + "comment" : "Menu theme for accessibility", "extractionState" : "manual", "localizations" : { "en" : { "stringUnit" : { "state" : "translated", - "value" : "Theme selected" + "value" : "Theme unselected" } }, "et" : { "stringUnit" : { "state" : "translated", - "value" : "Teema valitud" + "value" : "Teema valimata" } } } diff --git a/RIADigiDoc/UI/Component/AccessibilityView.swift b/RIADigiDoc/UI/Component/AccessibilityView.swift index 08461e47..5a052dce 100644 --- a/RIADigiDoc/UI/Component/AccessibilityView.swift +++ b/RIADigiDoc/UI/Component/AccessibilityView.swift @@ -54,4 +54,5 @@ struct AccessibilityView: View { AccessibilityView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/AdvancedSettingsView.swift b/RIADigiDoc/UI/Component/AdvancedSettingsView.swift index 0c4efda1..3cf04f81 100644 --- a/RIADigiDoc/UI/Component/AdvancedSettingsView.swift +++ b/RIADigiDoc/UI/Component/AdvancedSettingsView.swift @@ -24,10 +24,9 @@ struct AdvancedSettingsView: View { @AppTheme private var theme @AppTypography private var typography + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(LanguageSettings.self) private var languageSettings - @Environment(\.dismiss) private var dismiss - @Environment(NavigationPathManager.self) private var pathManager @State private var checkedAskRoleAndAddress = false @@ -105,14 +104,7 @@ struct AdvancedSettingsView: View { } Button( - action: { - Task { - await viewModel.restoreDefaultSettings() - Toast.show( - languageSettings.localized("Main settings use default settings message") - ) - } - }, + action: onRestoreDefaultSettingsClick, label: { Text(languageSettings.localized("Main settings use default settings button title")) .font(typography.labelLarge) @@ -128,6 +120,20 @@ struct AdvancedSettingsView: View { } ) } + + private func onRestoreDefaultSettingsClick() { + Task { + await viewModel.restoreDefaultSettings() + let message = + languageSettings.localized("Main settings use default settings message") + if voiceOverEnabled { + var saveButtonAccessibilityAnnouncement = AttributedString(message) + saveButtonAccessibilityAnnouncement.accessibilitySpeechAnnouncementPriority = .high + AccessibilityNotification.Announcement(saveButtonAccessibilityAnnouncement).post() + } + Toast.show(message) + } + } } // MARK: - Preview diff --git a/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsLinkRow.swift b/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsLinkRow.swift index 21be8945..a0dbbd1a 100644 --- a/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsLinkRow.swift +++ b/RIADigiDoc/UI/Component/AdvancedSettingsView/AdvancedSettingsLinkRow.swift @@ -41,12 +41,13 @@ struct AdvancedSettingsLinkRow: View { .scaledToFit() .frame(width: Dimensions.Icon.IconSizeXXS, height: Dimensions.Icon.IconSizeXXS) .foregroundStyle(theme.onSurface) - .accessibilityLabel(label.lowercased()) + .accessibilityHidden(true) } .contentShape(Rectangle()) .padding(.vertical, Dimensions.Padding.SPadding) } .buttonStyle(.plain) + .accessibilityLabel(label.lowercased()) } } diff --git a/RIADigiDoc/UI/Component/Container/CertificateDetailView.swift b/RIADigiDoc/UI/Component/Container/CertificateDetailView.swift index 2eea2c8f..76ad2135 100644 --- a/RIADigiDoc/UI/Component/Container/CertificateDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/CertificateDetailView.swift @@ -436,4 +436,5 @@ struct CertificateDetailView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/ContainerNotificationsView.swift b/RIADigiDoc/UI/Component/Container/ContainerNotificationsView.swift index 7580088e..394d6481 100644 --- a/RIADigiDoc/UI/Component/Container/ContainerNotificationsView.swift +++ b/RIADigiDoc/UI/Component/Container/ContainerNotificationsView.swift @@ -100,4 +100,5 @@ struct ContainerNotificationsView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift index 173cdd2b..eaa0287c 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/EncryptView.swift @@ -530,4 +530,5 @@ struct EncryptView: View { EncryptView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift b/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift index 65598b94..27be2c31 100644 --- a/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/Crypto/RecipientDetailView.swift @@ -177,4 +177,5 @@ struct RecipientDetailView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift index d7e3f93d..b3580455 100644 --- a/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift +++ b/RIADigiDoc/UI/Component/Container/SignatureDetailView.swift @@ -361,4 +361,5 @@ struct SignatureDetailView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/ActionMethodSelectionView.swift b/RIADigiDoc/UI/Component/Container/Signing/ActionMethodSelectionView.swift index 29c5d319..7247e619 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/ActionMethodSelectionView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/ActionMethodSelectionView.swift @@ -183,4 +183,5 @@ struct ActionMethodSelectionView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift index 40ac800b..8f2a8593 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/IdCard/IdCardView.swift @@ -170,4 +170,5 @@ struct IdCardView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift index de48a7f4..47f68f17 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/MobileId/MobileIdView.swift @@ -216,4 +216,5 @@ struct MobileIdView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift index 9137ac23..e6cb9301 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/NFC/NFCView.swift @@ -297,4 +297,5 @@ struct NFCView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift b/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift index f47a34e8..8f3f8d38 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/RoleView/RoleView.swift @@ -171,4 +171,5 @@ struct RoleView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift b/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift index c0c9b1ab..c71c8665 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SigningRootView.swift @@ -116,4 +116,7 @@ struct SigningRootView: View { #Preview { SigningRootView() + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift index 42a0d155..a2b83339 100644 --- a/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift +++ b/RIADigiDoc/UI/Component/Container/Signing/SmartId/SmartIdView.swift @@ -224,4 +224,5 @@ struct SmartIdView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift index 3204eb4c..0b2fd4b0 100644 --- a/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift +++ b/RIADigiDoc/UI/Component/CryptoFileOpeningView.swift @@ -105,4 +105,5 @@ struct CryptoFileOpeningView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/DiagnosticsView.swift b/RIADigiDoc/UI/Component/DiagnosticsView.swift index 2a0c01bd..c91b7f94 100644 --- a/RIADigiDoc/UI/Component/DiagnosticsView.swift +++ b/RIADigiDoc/UI/Component/DiagnosticsView.swift @@ -25,7 +25,7 @@ struct DiagnosticsView: View { @AppTheme private var theme @Environment(LanguageSettings.self) private var languageSettings - + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(\.dismiss) private var dismiss private let fileUtil: FileUtilProtocol @@ -55,16 +55,7 @@ struct DiagnosticsView: View { spacing: Dimensions.Padding.XXSPadding, content: { DiagnosticsHeaderButtons( - onCheckUpdateClick: { - Task { - let status = await viewModel.updateConfiguration() - if !status { - Toast.show( - languageSettings.localized("No Internet connection") - ) - } - } - }, + onCheckUpdateClick: onCheckUpdateClick, onSaveDiagnosticsClick: { Task { tempDiagnosticsFileURL = await viewModel.createLogFile( @@ -126,6 +117,21 @@ struct DiagnosticsView: View { } ) } + + private func onCheckUpdateClick() { + Task { + let status = await viewModel.updateConfiguration() + if !status { + let message = languageSettings.localized("No Internet connection") + if voiceOverEnabled { + var saveButtonAccessibilityAnnouncement = AttributedString(message) + saveButtonAccessibilityAnnouncement.accessibilitySpeechAnnouncementPriority = .high + AccessibilityNotification.Announcement(saveButtonAccessibilityAnnouncement).post() + } + Toast.show(message) + } + } + } } // MARK: - Preview @@ -133,4 +139,5 @@ struct DiagnosticsView: View { DiagnosticsView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/DiagnosticsView/PrimaryOutlinedButton.swift b/RIADigiDoc/UI/Component/DiagnosticsView/PrimaryOutlinedButton.swift index a070d41c..925b471e 100644 --- a/RIADigiDoc/UI/Component/DiagnosticsView/PrimaryOutlinedButton.swift +++ b/RIADigiDoc/UI/Component/DiagnosticsView/PrimaryOutlinedButton.swift @@ -55,11 +55,9 @@ struct PrimaryOutlinedButton: View { Text(verbatim: text) .foregroundStyle(isButtonEnabled ? theme.primary : theme.surfaceContainerHighest) .font(typography.labelLarge) - .lineLimit(nil) .multilineTextAlignment(.center) - .fixedSize(horizontal: false, vertical: true) - .frame(height: Dimensions.Icon.IconSizeXXS) - .padding(Dimensions.Padding.XSPadding) + .lineLimit(nil) + .frame(minHeight: Dimensions.Icon.IconSizeXXS) } .frame(maxWidth: .infinity) .padding(.vertical, Dimensions.Padding.XSPadding) diff --git a/RIADigiDoc/UI/Component/FileOpeningView.swift b/RIADigiDoc/UI/Component/FileOpeningView.swift index e5fee826..d4f1995c 100644 --- a/RIADigiDoc/UI/Component/FileOpeningView.swift +++ b/RIADigiDoc/UI/Component/FileOpeningView.swift @@ -132,4 +132,5 @@ struct FileOpeningView: View { ) .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/HomeView.swift b/RIADigiDoc/UI/Component/HomeView.swift index 8defd50f..dbd7c9d6 100644 --- a/RIADigiDoc/UI/Component/HomeView.swift +++ b/RIADigiDoc/UI/Component/HomeView.swift @@ -25,8 +25,10 @@ import UtilsLib struct HomeView: View { @AppTheme private var theme - @Environment(LanguageSettings.self) private var languageSettings + @AppTypography private var typography + @Environment(\.scenePhase) private var scenePhase + @Environment(LanguageSettings.self) private var languageSettings @Environment(NavigationPathManager.self) private var pathManager @State private var viewModel: HomeViewModel @@ -45,11 +47,13 @@ struct HomeView: View { @State private var showFilesBottomSheet: Bool = false @State private var showSignatureBottomSheet: Bool = false @State private var showCryptoBottomSheet: Bool = false + @State private var showHomeMenuBottomSheet: Bool = false @State private var containerType: ContainerType = .asice @State private var recentDocumentsExtensions: [String] = Constants.Container.ContainerExtensions - @Binding private var externalFiles: [URL] + @State private var sharedFilesLoadingTask: Task? + @AccessibilityFocusState private var isFilesButtonFocused: Bool private var filesBottomSheetActions: [BottomSheetButton] { HomeViewBottomSheetActions.actions( @@ -60,10 +64,10 @@ struct HomeView: View { containerType = .asice recentDocumentsExtensions = Constants.Container.ContainerExtensions pathManager.navigate(to: - .recentDocumentsView( - folderURL: getRecentDocumentsFolder(containerType: containerType), - extensions: recentDocumentsExtensions - ) + .recentDocumentsView( + folderURL: getRecentDocumentsFolder(containerType: containerType), + extensions: recentDocumentsExtensions + ) ) } ) @@ -78,88 +82,119 @@ struct HomeView: View { containerType = .cdoc recentDocumentsExtensions = Constants.Container.CryptoContainerExtensions pathManager.navigate(to: - .recentDocumentsView( - folderURL: getRecentDocumentsFolder(containerType: containerType), - extensions: recentDocumentsExtensions - ) + .recentDocumentsView( + folderURL: getRecentDocumentsFolder(containerType: containerType), + extensions: recentDocumentsExtensions + ) ) } ) } + private var homeMenuBottomSheetActions: [BottomSheetButton] { + HomeMenuBottomSheetActions.actions( + onInfoClick: { + pathManager.navigate(to: .infoView) + }, + onAccessibilityClick: { + pathManager.navigate(to: .accessibilityView) + }, + onDiagnosticsClick: { + pathManager.navigate(to: .diagnosticsView) + } + ) + } + + private var isBottomSheetPresented: Bool { + showFilesBottomSheet || showSignatureBottomSheet || showCryptoBottomSheet || showHomeMenuBottomSheet + } + init( fileOpeningViewModel: FileOpeningViewModel = Container.shared.fileOpeningViewModel(), cryptoFileOpeningViewModel: CryptoFileOpeningViewModel = Container.shared.cryptoFileOpeningViewModel(), - externalFiles: Binding<[URL]> ) { _viewModel = State(wrappedValue: Container.shared.homeViewModel()) _cryptoViewModel = State(wrappedValue: Container.shared.cryptoHomeViewModel()) self.fileOpeningViewModel = fileOpeningViewModel self.cryptoFileOpeningViewModel = cryptoFileOpeningViewModel - self._externalFiles = externalFiles } var body: some View { - VStack { - HomeHeader() - .padding(.bottom, Dimensions.Padding.LPadding) - - VStack(spacing: Dimensions.Padding.SPadding) { - SigningImportButton( - title: languageSettings.localized("Main home open document title"), - description: languageSettings.localized("Main home open document description"), - assetImageName: "ic_m3_attach_file_48pt_wght400", - isFileOpeningLoading: $isFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToSigningView, - showBottomSheet: $showFilesBottomSheet, - isImporting: $isImporting, - viewModel: viewModel - ) - .bottomSheet(isPresented: $showFilesBottomSheet, actions: filesBottomSheetActions) - - SigningImportButton( - title: languageSettings.localized("Signature"), - description: languageSettings.localized("Main home signature description"), - assetImageName: "ic_m3_stylus_note_48pt_wght400", - isFileOpeningLoading: $isFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToSigningView, - showBottomSheet: $showSignatureBottomSheet, - isImporting: $isImporting, - viewModel: viewModel - ) - .bottomSheet(isPresented: $showSignatureBottomSheet, actions: filesBottomSheetActions) - - CryptoImportButton( - title: languageSettings.localized("Main home crypto title"), - description: languageSettings.localized("Main home crypto description"), - assetImageName: "ic_m3_encrypted_48pt_wght400", - isFileOpeningLoading: $isCryptoFileOpeningLoading, - isNavigatingToNextView: $isNavigatingToEncryptView, - showBottomSheet: $showCryptoBottomSheet, - isImporting: $isCryptoImporting, - viewModel: cryptoViewModel - ) - .bottomSheet(isPresented: $showCryptoBottomSheet, actions: cryptoFilesBottomSheetActions) - - ActionButton( - title: languageSettings.localized("Main home my eid title"), - description: languageSettings.localized("Main home my eid description"), - assetImageName: "ic_m3_co_present_48pt_wght400", - action: { - pathManager.navigate(to: .myEidRootView) + TopBarContainer( + leftIcon: "ic_m3_menu_48pt_wght400", + leftIconAccessibility: "Menu", + onLeftClick: { + showHomeMenuBottomSheet = true + }, + onSettingsSheetDismiss: { + focusFilesButtonWithDelay() + }, + content: { + ScrollView { + HomeHeader() + .padding(.bottom, Dimensions.Padding.LPadding) + + VStack(spacing: Dimensions.Padding.SPadding) { + SigningImportButton( + title: languageSettings.localized("Main home open document title"), + description: languageSettings.localized("Main home open document description"), + assetImageName: "ic_m3_attach_file_48pt_wght400", + isFileOpeningLoading: $isFileOpeningLoading, + isNavigatingToNextView: $isNavigatingToSigningView, + showBottomSheet: $showFilesBottomSheet, + isImporting: $isImporting, + viewModel: viewModel + ) + .bottomSheet(isPresented: $showFilesBottomSheet, actions: filesBottomSheetActions) + .accessibilityFocused($isFilesButtonFocused) + + SigningImportButton( + title: languageSettings.localized("Signature"), + description: languageSettings.localized("Main home signature description"), + assetImageName: "ic_m3_stylus_note_48pt_wght400", + isFileOpeningLoading: $isFileOpeningLoading, + isNavigatingToNextView: $isNavigatingToSigningView, + showBottomSheet: $showSignatureBottomSheet, + isImporting: $isImporting, + viewModel: viewModel + ) + .bottomSheet(isPresented: $showSignatureBottomSheet, actions: filesBottomSheetActions) + + CryptoImportButton( + title: languageSettings.localized("Main home crypto title"), + description: languageSettings.localized("Main home crypto description"), + assetImageName: "ic_m3_encrypted_48pt_wght400", + isFileOpeningLoading: $isCryptoFileOpeningLoading, + isNavigatingToNextView: $isNavigatingToEncryptView, + showBottomSheet: $showCryptoBottomSheet, + isImporting: $isCryptoImporting, + viewModel: cryptoViewModel + ) + .bottomSheet(isPresented: $showCryptoBottomSheet, actions: cryptoFilesBottomSheetActions) + + ActionButton( + title: languageSettings.localized("Main home my eid title"), + description: languageSettings.localized("Main home my eid description"), + assetImageName: "ic_m3_co_present_48pt_wght400", + action: { + pathManager.navigate(to: .myEidRootView) + } + ) } - ) - } - .padding(Dimensions.Padding.SPadding) + .padding(Dimensions.Padding.SPadding) - Spacer() + Spacer() + } + } + ) + .bottomSheet(isPresented: $showHomeMenuBottomSheet, actions: homeMenuBottomSheetActions) + .onOpenURL { url in + handleFiles([url]) } - .onChange(of: externalFiles) { _, extFiles in - if !extFiles.isEmpty { - isFileOpeningLoading = true - viewModel.isImporting = false - self.viewModel.setChosenFiles(.success(extFiles)) - externalFiles = [] + .onAppear { + focusFilesButtonWithDelay() + if scenePhase == .active { + loadSharedFiles() } } .onChange(of: isNavigatingToSigningView, { _, newValue in @@ -176,6 +211,26 @@ struct HomeView: View { isNavigatingToEncryptView = false } }) + .onChange(of: isBottomSheetPresented) { oldValue, newValue in + if oldValue && !newValue { + focusFilesButtonWithDelay() + } + } + .onChange(of: scenePhase) { _, newPhase in + if newPhase == .active { + sharedFilesLoadingTask?.cancel() + loadSharedFiles() + } + } + .onDisappear { + sharedFilesLoadingTask?.cancel() + } + } + + private func focusFilesButtonWithDelay() { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { + isFilesButtonFocused = true + } } func getRecentDocumentsFolder(containerType: ContainerType) -> URL? { @@ -186,10 +241,25 @@ struct HomeView: View { return viewModel.getRecentDocumentsFolder() } } + + private func loadSharedFiles() { + sharedFilesLoadingTask = Task { + let sharedFiles = await viewModel.getSharedFiles() + handleFiles(sharedFiles) + } + } + + private func handleFiles(_ files: [URL]) { + if !files.isEmpty { + isFileOpeningLoading = true + viewModel.isImporting = false + viewModel.setChosenFiles(.success(files)) + } + } } #Preview { - HomeView(externalFiles: .constant([])) + HomeView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) .environment(NavigationPathManager()) diff --git a/RIADigiDoc/UI/Component/HomeView/HomeHeader.swift b/RIADigiDoc/UI/Component/HomeView/HomeHeader.swift index e2f6092b..8a95dd34 100644 --- a/RIADigiDoc/UI/Component/HomeView/HomeHeader.swift +++ b/RIADigiDoc/UI/Component/HomeView/HomeHeader.swift @@ -24,45 +24,40 @@ import FactoryKit struct HomeHeader: View { @AppTheme private var theme @AppTypography private var typography + @Environment(LanguageSettings.self) private var languageSettings var body: some View { VStack(spacing: Dimensions.Padding.XXSPadding ) { - LogoComponent() - VersionComponent() + logoComponent + versionComponent } .padding(.vertical, Dimensions.Padding.XXSPadding) .padding(.horizontal, Dimensions.Padding.XSPadding) } -} -// MARK: - Logo Component -private struct LogoComponent: View { - @AppTheme private var theme - @AppTypography private var typography - @Environment(LanguageSettings.self) private var languageSettings + private var logoText: String { + languageSettings.localized("DigiDoc") + } - var body: some View { + @ViewBuilder + private var logoComponent: some View { HStack(spacing: Dimensions.Padding.XSPadding) { Image("image_id_ee") .resizable() .scaledToFit() .frame(width: Dimensions.Icon.IconSizeM) - .accessibilityLabel(languageSettings.localized("DigiDoc")) - Text(verbatim: languageSettings.localized("DigiDoc")) + Text(verbatim: logoText) .font(typography.displayMedium) .foregroundStyle(theme.onSurface) } + .accessibilityElement(children: .combine) + .accessibilityLabel(logoText) + .accessibilityAddTraits(.isImage) } -} - -// MARK: - Version Component -private struct VersionComponent: View { - @AppTheme private var theme - @AppTypography private var typography - @Environment(LanguageSettings.self) private var languageSettings - var body: some View { + @ViewBuilder + private var versionComponent: some View { Text(verbatim: languageSettings.localized( "Main home version", [BundleUtil.getAppVersion()]) diff --git a/RIADigiDoc/UI/Component/InfoView.swift b/RIADigiDoc/UI/Component/InfoView.swift index b5ac727c..19548022 100644 --- a/RIADigiDoc/UI/Component/InfoView.swift +++ b/RIADigiDoc/UI/Component/InfoView.swift @@ -53,4 +53,5 @@ struct InfoView: View { InfoView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/LanguageChooserView.swift b/RIADigiDoc/UI/Component/LanguageChooserView.swift index 55d8d71a..1e19b98b 100644 --- a/RIADigiDoc/UI/Component/LanguageChooserView.swift +++ b/RIADigiDoc/UI/Component/LanguageChooserView.swift @@ -55,7 +55,7 @@ struct LanguageChooserView: View { let title = languageSettings.localized(languageOption.titleKey) let selected = isSelected ? languageSettings.localized("Menu language selected") - : languageSettings.localized("Menu language") + : languageSettings.localized("Menu language unselected") return "\(title) \(selected)" }, accessibilityInputLabel: { languageOption in @@ -74,4 +74,5 @@ struct LanguageChooserView: View { LanguageChooserView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/My eID/MyEidRootView.swift b/RIADigiDoc/UI/Component/My eID/MyEidRootView.swift index 3ed947ee..eb428994 100644 --- a/RIADigiDoc/UI/Component/My eID/MyEidRootView.swift +++ b/RIADigiDoc/UI/Component/My eID/MyEidRootView.swift @@ -73,4 +73,7 @@ struct MyEidRootView: View { #Preview { MyEidRootView() + .environment(Container.shared.languageSettings()) + .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/ProxySettingsView.swift b/RIADigiDoc/UI/Component/ProxySettingsView.swift index db4aaad7..11505569 100644 --- a/RIADigiDoc/UI/Component/ProxySettingsView.swift +++ b/RIADigiDoc/UI/Component/ProxySettingsView.swift @@ -23,6 +23,7 @@ import FactoryKit struct ProxySettingsView: View { @AppTheme private var theme @AppTypography private var typography + @Environment(\.accessibilityVoiceOverEnabled) private var voiceOverEnabled @Environment(LanguageSettings.self) private var languageSettings @Environment(\.dismiss) private var dismiss @@ -94,6 +95,12 @@ struct ProxySettingsView: View { ? languageSettings.localized("Main settings proxy check connection success") : languageSettings.localized("Main settings proxy check connection unsuccessful") Toast.show(message) + + if voiceOverEnabled { + var saveButtonAccessibilityAnnouncement = AttributedString(message) + saveButtonAccessibilityAnnouncement.accessibilitySpeechAnnouncementPriority = .high + AccessibilityNotification.Announcement(saveButtonAccessibilityAnnouncement).post() + } } }, label: { @@ -162,4 +169,5 @@ struct ProxySettingsView: View { ProxySettingsView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/Shared/BottomSheet.swift b/RIADigiDoc/UI/Component/Shared/BottomSheet.swift index 4eac1bb8..4ce59725 100644 --- a/RIADigiDoc/UI/Component/Shared/BottomSheet.swift +++ b/RIADigiDoc/UI/Component/Shared/BottomSheet.swift @@ -26,6 +26,8 @@ struct BottomSheet: View { @AppTheme private var theme @AppTypography private var typography + @AccessibilityFocusState private var focusedButtonTitle: String? + let actions: [BottomSheetButton] var body: some View { @@ -84,12 +86,20 @@ struct BottomSheet: View { .foregroundStyle(theme.onSurface) .padding(.horizontal, Dimensions.Padding.MPadding) }) + .accessibilityFocused($focusedButtonTitle, equals: item.title) } } } .padding(.vertical, Dimensions.Padding.MSPadding) .padding(.bottom, Dimensions.Padding.MPadding) .frame(maxWidth: .infinity) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.7) { + if let firstAction = actions.first(where: { $0.showButton }) { + focusedButtonTitle = firstAction.title + } + } + } } } diff --git a/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift b/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift index 6e0b6f0f..e038031f 100644 --- a/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift +++ b/RIADigiDoc/UI/Component/Shared/TopBar/TopBar.swift @@ -37,7 +37,7 @@ struct TopBarContainer: View { var onLeftClick: () -> Void = {} var rightPrimaryIcon: String = "ic_m3_help_48pt_wght400" - var rightPrimaryIconAccessibility: String = "link www.id.ee" + var rightPrimaryIconAccessibility: String = "Helpdesk" var rightPrimaryIconAccessibilityInput: String = "Helpdesk" var onRightPrimaryClick: (() -> Void)? @@ -45,6 +45,7 @@ struct TopBarContainer: View { var rightSecondaryIconAccessibility: String = "Settings" var rightSecondaryIconAccessibilityInput: String? var onRightSecondaryClick: (() -> Void)? + var onSettingsSheetDismiss: (() -> Void)? var extraButtonIcon: String = "ic_m3_notifications_48pt_wght400" var extraButtonIconAccessibility: String = "Container notifications" @@ -106,6 +107,11 @@ struct TopBarContainer: View { content() } .bottomSheet(isPresented: $showSettingsSheet, actions: buildBottomSheetActions()) + .onChange(of: showSettingsSheet) { oldValue, newValue in + if oldValue && !newValue { + onSettingsSheetDismiss?() + } + } .navigationBarHidden(true) .navigationBarBackButtonHidden(true) .background(theme.surface) diff --git a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift index a92e39e9..f63c4e4f 100644 --- a/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift +++ b/RIADigiDoc/UI/Component/SigningServicesSettingsView.swift @@ -35,9 +35,6 @@ struct SigningServicesSettingsView: View { TopBarContainer( title: languageSettings.localized("Main settings signing services title"), onLeftClick: { dismiss() }, - onRightSecondaryClick: { - showSettingsBottomSheetFromButton = true - }, excludeDestinations: [.advanced], content: { ScrollView { @@ -71,4 +68,5 @@ struct SigningServicesSettingsView: View { SigningServicesSettingsView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/UI/Component/ThemeChooserView.swift b/RIADigiDoc/UI/Component/ThemeChooserView.swift index bbf9f852..6ffa2377 100644 --- a/RIADigiDoc/UI/Component/ThemeChooserView.swift +++ b/RIADigiDoc/UI/Component/ThemeChooserView.swift @@ -52,7 +52,7 @@ struct ThemeChooserView: View { let title = languageSettings.localized(themeOption.titleKey) let selected = isSelected ? languageSettings.localized("Menu theme selected") - : languageSettings.localized("Menu theme") + : languageSettings.localized("Menu theme unselected") return "\(title) \(selected)" } ) @@ -68,4 +68,5 @@ struct ThemeChooserView: View { ThemeChooserView() .environment(Container.shared.languageSettings()) .environment(Container.shared.themeSettings()) + .environment(NavigationPathManager()) } diff --git a/RIADigiDoc/ViewModel/ContentViewModel.swift b/RIADigiDoc/ViewModel/ContentViewModel.swift deleted file mode 100644 index b9e064ea..00000000 --- a/RIADigiDoc/ViewModel/ContentViewModel.swift +++ /dev/null @@ -1,62 +0,0 @@ -/* - * Copyright 2017 - 2025 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 Foundation -import FactoryKit -import UtilsLib -import CommonsLib - -@Observable -@MainActor -class ContentViewModel: ContentViewModelProtocol, Loggable { - private let fileUtil: FileUtilProtocol - private let fileManager: FileManagerProtocol - - init( - fileUtil: FileUtilProtocol, - fileManager: FileManagerProtocol - ) { - self.fileUtil = fileUtil - self.fileManager = fileManager - } - - func getSharedFiles() async -> [URL] { - do { - ContentViewModel.logger().debug("Checking for shared files...") - let sharedFolderURL = try await Directories.getSharedFolder(fileManager: fileManager) - .validURL(fileUtil: fileUtil) - - let contents = try fileManager.contentsOfDirectory( - at: sharedFolderURL, - includingPropertiesForKeys: nil, - options: .skipsHiddenFiles) - - if contents.isEmpty { - ContentViewModel.logger().debug("Shared files folder is empty") - } else { - ContentViewModel.logger().debug("Found \(contents.count) shared files") - } - - return contents - } catch { - ContentViewModel.logger().error("Unable to get shared files: \(error.localizedDescription)") - return [] - } - } -} diff --git a/RIADigiDoc/ViewModel/HomeViewModel.swift b/RIADigiDoc/ViewModel/HomeViewModel.swift index e3f7a66d..5ee5ca5d 100644 --- a/RIADigiDoc/ViewModel/HomeViewModel.swift +++ b/RIADigiDoc/ViewModel/HomeViewModel.swift @@ -34,13 +34,16 @@ class HomeViewModel: HomeViewModelProtocol, Loggable { private let sharedContainerViewModel: SharedContainerViewModelProtocol private let fileManager: FileManagerProtocol + private let fileUtil: FileUtilProtocol init( sharedContainerViewModel: SharedContainerViewModelProtocol, - fileManager: FileManagerProtocol + fileManager: FileManagerProtocol, + fileUtil: FileUtilProtocol ) { self.sharedContainerViewModel = sharedContainerViewModel self.fileManager = fileManager + self.fileUtil = fileUtil } func didUserCancelFileOpening(isImportingValue: Bool, isFileOpeningLoading: Bool) -> Bool { @@ -65,4 +68,28 @@ class HomeViewModel: HomeViewModelProtocol, Loggable { return nil } } + + func getSharedFiles() async -> [URL] { + do { + HomeViewModel.logger().debug("Checking for shared files...") + let sharedFolderURL = try await Directories.getSharedFolder(fileManager: fileManager) + .validURL(fileUtil: fileUtil) + + let contents = try fileManager.contentsOfDirectory( + at: sharedFolderURL, + includingPropertiesForKeys: nil, + options: .skipsHiddenFiles) + + if contents.isEmpty { + HomeViewModel.logger().debug("Shared files folder is empty") + } else { + HomeViewModel.logger().debug("Found \(contents.count) shared files") + } + + return contents + } catch { + HomeViewModel.logger().error("Unable to get shared files: \(error.localizedDescription)") + return [] + } + } } diff --git a/RIADigiDoc/ViewModel/Protocols/ContentViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/ContentViewModelProtocol.swift deleted file mode 100644 index 99000a47..00000000 --- a/RIADigiDoc/ViewModel/Protocols/ContentViewModelProtocol.swift +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2017 - 2025 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 Foundation - -/// @mockable -@MainActor -public protocol ContentViewModelProtocol: Sendable { - func getSharedFiles() async -> [URL] -} diff --git a/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift b/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift index 3c8ccc5f..bd716e9a 100644 --- a/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift +++ b/RIADigiDoc/ViewModel/Protocols/HomeViewModelProtocol.swift @@ -26,4 +26,5 @@ public protocol HomeViewModelProtocol: Sendable { func didUserCancelFileOpening(isImportingValue: Bool, isFileOpeningLoading: Bool) -> Bool func setChosenFiles(_ chosenFiles: Result<[URL], Error>) func getRecentDocumentsFolder() -> URL? + func getSharedFiles() async -> [URL] } diff --git a/RIADigiDocTests/ViewModel/HomeViewModelTests.swift b/RIADigiDocTests/ViewModel/HomeViewModelTests.swift index 596c81dd..583ecab3 100644 --- a/RIADigiDocTests/ViewModel/HomeViewModelTests.swift +++ b/RIADigiDocTests/ViewModel/HomeViewModelTests.swift @@ -21,19 +21,24 @@ import Foundation import Testing import CommonsLib import CommonsLibMocks +import UtilsLib +import UtilsLibMocks @MainActor struct HomeViewModelTests { private let mockSharedContainerViewModel: SharedContainerViewModelProtocolMock private let mockFileManager: FileManagerProtocolMock + private let mockFileUtil: FileUtilProtocolMock private let viewModel: HomeViewModel init() async throws { mockSharedContainerViewModel = SharedContainerViewModelProtocolMock() mockFileManager = FileManagerProtocolMock() + mockFileUtil = FileUtilProtocolMock() viewModel = HomeViewModel( sharedContainerViewModel: mockSharedContainerViewModel, - fileManager: mockFileManager + fileManager: mockFileManager, + fileUtil: mockFileUtil ) }