feat: Implement persistent logout on 401 errors and hide TabBar in detail views
- Add AppViewModel to manage app-level state and handle 401 responses - Implement automatic logout when API returns 401 Unauthorized - Add persistent logout state using existing hasFinishedSetup flag - Move NavigationStack outside TabView to enable automatic TabBar hiding - Update API classes to send UnauthorizedAPIResponse notifications - TabBar now hides automatically when navigating to detail views
This commit is contained in:
parent
660f271982
commit
953ff5da8d
@ -48,6 +48,9 @@
|
|||||||
},
|
},
|
||||||
"%lld min" : {
|
"%lld min" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"%lld minutes" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"%lld." : {
|
"%lld." : {
|
||||||
|
|
||||||
@ -99,6 +102,12 @@
|
|||||||
},
|
},
|
||||||
"Are you sure you want to log out? This will delete all your login credentials and return you to setup." : {
|
"Are you sure you want to log out? This will delete all your login credentials and return you to setup." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Automatic sync" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Automatically mark articles as read" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Available tags" : {
|
"Available tags" : {
|
||||||
|
|
||||||
@ -111,6 +120,9 @@
|
|||||||
},
|
},
|
||||||
"Changes take effect immediately. Lower log levels include higher ones (Debug includes all, Critical includes only critical messages)." : {
|
"Changes take effect immediately. Lower log levels include higher ones (Debug includes all, Critical includes only critical messages)." : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Clear cache" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Close" : {
|
"Close" : {
|
||||||
|
|
||||||
@ -120,6 +132,9 @@
|
|||||||
},
|
},
|
||||||
"Critical" : {
|
"Critical" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Data Management" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Debug" : {
|
"Debug" : {
|
||||||
|
|
||||||
@ -258,6 +273,9 @@
|
|||||||
},
|
},
|
||||||
"OK" : {
|
"OK" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Open external links in in-app Safari" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Optional: Custom title" : {
|
"Optional: Custom title" : {
|
||||||
|
|
||||||
@ -301,12 +319,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"Reading Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Remove" : {
|
"Remove" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Reset" : {
|
"Reset" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Reset settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Reset to Defaults" : {
|
"Reset to Defaults" : {
|
||||||
|
|
||||||
@ -316,6 +340,9 @@
|
|||||||
},
|
},
|
||||||
"Resume listening" : {
|
"Resume listening" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Safari Reader Mode" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Save bookmark" : {
|
"Save bookmark" : {
|
||||||
|
|
||||||
@ -364,6 +391,12 @@
|
|||||||
},
|
},
|
||||||
"Speed" : {
|
"Speed" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Sync interval" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"Sync Settings" : {
|
||||||
|
|
||||||
},
|
},
|
||||||
"Syncing with server..." : {
|
"Syncing with server..." : {
|
||||||
|
|
||||||
|
|||||||
@ -39,6 +39,11 @@ class SimpleAPI {
|
|||||||
logger.logNetworkRequest(method: "POST", url: "/api/bookmarks", statusCode: httpResponse.statusCode)
|
logger.logNetworkRequest(method: "POST", url: "/api/bookmarks", statusCode: httpResponse.statusCode)
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
if httpResponse.statusCode == 401 {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("UnauthorizedAPIResponse"), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
let msg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
let msg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||||
logger.error("Server error \(httpResponse.statusCode): \(msg)")
|
logger.error("Server error \(httpResponse.statusCode): \(msg)")
|
||||||
showStatus("Server error: \(httpResponse.statusCode)\n\(msg)", true)
|
showStatus("Server error: \(httpResponse.statusCode)\n\(msg)", true)
|
||||||
@ -87,6 +92,11 @@ class SimpleAPI {
|
|||||||
logger.logNetworkRequest(method: "GET", url: "/api/bookmarks/labels", statusCode: httpResponse.statusCode)
|
logger.logNetworkRequest(method: "GET", url: "/api/bookmarks/labels", statusCode: httpResponse.statusCode)
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
if httpResponse.statusCode == 401 {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("UnauthorizedAPIResponse"), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
let msg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
let msg = String(data: data, encoding: .utf8) ?? "Unknown error"
|
||||||
logger.error("Server error \(httpResponse.statusCode): \(msg)")
|
logger.error("Server error \(httpResponse.statusCode): \(msg)")
|
||||||
showStatus("Server error: \(httpResponse.statusCode)\n\(msg)", true)
|
showStatus("Server error: \(httpResponse.statusCode)\n\(msg)", true)
|
||||||
|
|||||||
41
documentation/401.md
Normal file
41
documentation/401.md
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# Feature: Persistentes Logout bei 401 Unauthorized
|
||||||
|
|
||||||
|
## Problemstellung
|
||||||
|
Wenn eine API-Anfrage mit einem `401 Unauthorized`-Response fehlschlägt, bedeutet dies, dass der aktuell gespeicherte Token oder die Session des Nutzers ungültig ist (z. B. durch manuelles Löschen des Tokens im Backend oder andere Ursachen).
|
||||||
|
In diesem Zustand darf der User nicht weiter mit einer scheinbar gültigen Session in der App interagieren.
|
||||||
|
|
||||||
|
## Ziel
|
||||||
|
Die App soll den Nutzer in einem solchen Fall automatisch vollständig **ausloggen** und auf den **Setup-/Login-Screen** umleiten.
|
||||||
|
Dies muss **persistiert** sein, d. h. auch nach App-Neustart darf der Nutzer nicht eingeloggt zurückkehren, solange keine erfolgreiche neue Anmeldung durchgeführt wurde.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Anforderungen
|
||||||
|
|
||||||
|
1. **Erkennen von ungültigem Token**
|
||||||
|
- Jede API-Antwort mit `401 Unauthorized` löst den Logout-Prozess aus.
|
||||||
|
- Optional: Kontext beachten (z. B. ob der Request ein Refresh-Token war).
|
||||||
|
|
||||||
|
2. **Logout-Mechanismus**
|
||||||
|
- Alle gespeicherten Zugangsdaten (Access Token, Refresh Token, User-Daten im Keychain/Storage) werden gelöscht.
|
||||||
|
- UI-State wird in den "nicht eingeloggten" Zustand zurückversetzt.
|
||||||
|
- Persistenter "loggedOut"-State wird gesetzt (z. B. in `UserDefaults` oder einer App-State-DB).
|
||||||
|
|
||||||
|
3. **Persistenz**
|
||||||
|
- Falls der Nutzer die App neu startet, startet er im Setup-/Login-Screen und nicht in einem alten Session-Kontext.
|
||||||
|
|
||||||
|
4. **Wiederanmeldung**
|
||||||
|
- Sobald der Nutzer sich neu einloggt und erfolgreich ein Access Token erhält:
|
||||||
|
- wird der persistente "loggedOut"-State zurückgesetzt
|
||||||
|
- die App verhält sich wieder wie gewohnt im eingeloggten Zustand.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Beispiel-Use Case
|
||||||
|
- User ist eingeloggt in die App.
|
||||||
|
- Im Backend wird manuell der Token gelöscht oder die Session invalidiert.
|
||||||
|
- Nächster API-Call → API gibt `401 Unauthorized` zurück.
|
||||||
|
- App erkennt ungültigen Zustand, **löscht alle Tokens und Session-Daten** und leitet den User sofort auf den **Setup-/Login-Screen** um.
|
||||||
|
- Auch nach einem App-Neustart startet der User weiterhin im Setup-Screen.
|
||||||
|
- Sobald der User sich erfolgreich einloggt, gelten alle API-Calls wieder normal.
|
||||||
|
|
||||||
18
documentation/tabbar.md
Normal file
18
documentation/tabbar.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
## Feature: TabView nur in Root-Screens sichtbar, nicht in Detail-Views
|
||||||
|
|
||||||
|
### Beschreibung
|
||||||
|
Die App verwendet aktuell eine `TabView` (bzw. einen Tab Controller), die global sichtbar ist.
|
||||||
|
Der Workflow funktioniert grundsätzlich, allerdings wird die `TabView` auch dann angezeigt, wenn ein Benutzer von einem Tab in eine **Detailansicht** (z. B. Artikel-Detail, Item-Detail) navigiert.
|
||||||
|
|
||||||
|
Ziel ist es, dass die `TabView` **nur in den Root-Views** sichtbar ist.
|
||||||
|
Beim Öffnen einer **Detail-Ansicht** soll die `TabView` automatisch ausgeblendet werden, damit dort alternativ eine eigene **Bottom Toolbar** angezeigt werden kann.
|
||||||
|
|
||||||
|
### Akzeptanzkriterien
|
||||||
|
- `TabView` ist standardmäßig in den Haupt-Tabs sichtbar.
|
||||||
|
- Navigiert der Nutzer in eine Detail-Ansicht (z. B. Rom Detail), wird die `TabView` ausgeblendet.
|
||||||
|
- In den Detail-Ansichten kann ein eigener `Toolbar` oder eine Custom Bottom Bar sichtbar sein - ist aber kein teil von diesem task.
|
||||||
|
- Navigation zurück zur Root-View blendet die `TabView` wieder ein.
|
||||||
|
|
||||||
|
# Technischer hinweis
|
||||||
|
|
||||||
|
To hide TabBar when we jumps towards next screen we just have to place NavigationView to the right place. Makesure Embed TabView inside NavigationView so creating unique Navigationview for both tabs.
|
||||||
@ -41,6 +41,14 @@ class API: PAPI {
|
|||||||
return url
|
return url
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func handleUnauthorizedResponse(_ statusCode: Int) {
|
||||||
|
if statusCode == 401 {
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
NotificationCenter.default.post(name: NSNotification.Name("UnauthorizedAPIResponse"), object: nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func makeJSONRequestWithHeaders<T: Codable>(
|
private func makeJSONRequestWithHeaders<T: Codable>(
|
||||||
endpoint: String,
|
endpoint: String,
|
||||||
@ -74,6 +82,7 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,6 +123,7 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -146,6 +156,7 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -181,6 +192,7 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||||
logger.logNetworkError(method: "POST", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
logger.logNetworkError(method: "POST", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
@ -342,6 +354,7 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||||
logger.logNetworkError(method: "PATCH", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
logger.logNetworkError(method: "PATCH", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
@ -379,6 +392,7 @@ class API: PAPI {
|
|||||||
}
|
}
|
||||||
|
|
||||||
guard 200...299 ~= httpResponse.statusCode else {
|
guard 200...299 ~= httpResponse.statusCode else {
|
||||||
|
handleUnauthorizedResponse(httpResponse.statusCode)
|
||||||
logger.logNetworkError(method: "DELETE", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
logger.logNetworkError(method: "DELETE", url: url.absoluteString, error: APIError.serverError(httpResponse.statusCode))
|
||||||
throw APIError.serverError(httpResponse.statusCode)
|
throw APIError.serverError(httpResponse.statusCode)
|
||||||
}
|
}
|
||||||
|
|||||||
71
readeck/UI/AppViewModel.swift
Normal file
71
readeck/UI/AppViewModel.swift
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
//
|
||||||
|
// AppViewModel.swift
|
||||||
|
// readeck
|
||||||
|
//
|
||||||
|
// Created by Ilyas Hallak on 27.08.25.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
class AppViewModel: ObservableObject {
|
||||||
|
private let settingsRepository = SettingsRepository()
|
||||||
|
private let logoutUseCase: LogoutUseCase
|
||||||
|
|
||||||
|
@Published var hasFinishedSetup: Bool = true
|
||||||
|
|
||||||
|
init(logoutUseCase: LogoutUseCase = LogoutUseCase()) {
|
||||||
|
self.logoutUseCase = logoutUseCase
|
||||||
|
setupNotificationObservers()
|
||||||
|
|
||||||
|
Task {
|
||||||
|
await loadSetupStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupNotificationObservers() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: NSNotification.Name("UnauthorizedAPIResponse"),
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
Task {
|
||||||
|
await self?.handleUnauthorizedResponse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: NSNotification.Name("SetupStatusChanged"),
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.loadSetupStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func handleUnauthorizedResponse() async {
|
||||||
|
print("AppViewModel: Handling 401 Unauthorized - logging out user")
|
||||||
|
|
||||||
|
do {
|
||||||
|
// Führe den Logout durch
|
||||||
|
try await logoutUseCase.execute()
|
||||||
|
|
||||||
|
// Update UI state
|
||||||
|
loadSetupStatus()
|
||||||
|
|
||||||
|
print("AppViewModel: User successfully logged out due to 401 error")
|
||||||
|
} catch {
|
||||||
|
print("AppViewModel: Error during logout: \(error)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
private func loadSetupStatus() {
|
||||||
|
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
||||||
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -18,12 +18,14 @@ struct PhoneTabView: View {
|
|||||||
@EnvironmentObject var appSettings: AppSettings
|
@EnvironmentObject var appSettings: AppSettings
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
GlobalPlayerContainerView {
|
NavigationStack {
|
||||||
TabView(selection: $selectedTabIndex) {
|
GlobalPlayerContainerView {
|
||||||
mainTabsContent
|
TabView(selection: $selectedTabIndex) {
|
||||||
moreTabContent
|
mainTabsContent
|
||||||
|
moreTabContent
|
||||||
|
}
|
||||||
|
.accentColor(.accentColor)
|
||||||
}
|
}
|
||||||
.accentColor(.accentColor)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,23 +36,19 @@ struct PhoneTabView: View {
|
|||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var mainTabsContent: some View {
|
private var mainTabsContent: some View {
|
||||||
ForEach(Array(mainTabs.enumerated()), id: \.element) { idx, tab in
|
ForEach(Array(mainTabs.enumerated()), id: \.element) { idx, tab in
|
||||||
NavigationStack {
|
tabView(for: tab)
|
||||||
tabView(for: tab)
|
.tabItem {
|
||||||
}
|
Label(tab.label, systemImage: tab.systemImage)
|
||||||
.tabItem {
|
}
|
||||||
Label(tab.label, systemImage: tab.systemImage)
|
.tag(idx)
|
||||||
}
|
|
||||||
.tag(idx)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var moreTabContent: some View {
|
private var moreTabContent: some View {
|
||||||
NavigationStack {
|
VStack(spacing: 0) {
|
||||||
VStack(spacing: 0) {
|
moreTabsList
|
||||||
moreTabsList
|
moreTabsFooter
|
||||||
moreTabsFooter
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.tabItem {
|
.tabItem {
|
||||||
Label("More", systemImage: "ellipsis")
|
Label("More", systemImage: "ellipsis")
|
||||||
@ -71,6 +69,7 @@ struct PhoneTabView: View {
|
|||||||
NavigationLink {
|
NavigationLink {
|
||||||
tabView(for: tab)
|
tabView(for: tab)
|
||||||
.navigationTitle(tab.label)
|
.navigationTitle(tab.label)
|
||||||
|
.navigationBarTitleDisplayMode(.large)
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
// tags and search handle navigation by own
|
// tags and search handle navigation by own
|
||||||
if tab != .tags && tab != .search {
|
if tab != .tags && tab != .search {
|
||||||
|
|||||||
@ -10,13 +10,13 @@ import netfox
|
|||||||
|
|
||||||
@main
|
@main
|
||||||
struct readeckApp: App {
|
struct readeckApp: App {
|
||||||
@State private var hasFinishedSetup = true
|
@StateObject private var appViewModel = AppViewModel()
|
||||||
@StateObject private var appSettings = AppSettings()
|
@StateObject private var appSettings = AppSettings()
|
||||||
|
|
||||||
var body: some Scene {
|
var body: some Scene {
|
||||||
WindowGroup {
|
WindowGroup {
|
||||||
Group {
|
Group {
|
||||||
if hasFinishedSetup {
|
if appViewModel.hasFinishedSetup {
|
||||||
MainTabView()
|
MainTabView()
|
||||||
} else {
|
} else {
|
||||||
SettingsServerView()
|
SettingsServerView()
|
||||||
@ -32,25 +32,19 @@ struct readeckApp: App {
|
|||||||
// Initialize server connectivity monitoring
|
// Initialize server connectivity monitoring
|
||||||
_ = ServerConnectivity.shared
|
_ = ServerConnectivity.shared
|
||||||
Task {
|
Task {
|
||||||
await loadSetupStatus()
|
await loadAppSettings()
|
||||||
}
|
|
||||||
}
|
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("SetupStatusChanged"))) { _ in
|
|
||||||
Task {
|
|
||||||
await loadSetupStatus()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("SettingsChanged"))) { _ in
|
.onReceive(NotificationCenter.default.publisher(for: NSNotification.Name("SettingsChanged"))) { _ in
|
||||||
Task {
|
Task {
|
||||||
await loadSetupStatus()
|
await loadAppSettings()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private func loadSetupStatus() async {
|
private func loadAppSettings() async {
|
||||||
let settingsRepository = SettingsRepository()
|
let settingsRepository = SettingsRepository()
|
||||||
hasFinishedSetup = settingsRepository.hasFinishedSetup
|
|
||||||
let settings = try? await settingsRepository.loadSettings()
|
let settings = try? await settingsRepository.loadSettings()
|
||||||
await MainActor.run {
|
await MainActor.run {
|
||||||
appSettings.settings = settings
|
appSettings.settings = settings
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user