fix: Prevent sync during onboarding and improve Share Extension UX
- Add guard checks in AppViewModel to prevent sync when hasFinishedSetup is false - Share Extension now shows friendly UI when app not configured or session expired - Check token existence via KeychainHelper instead of attempting API calls - Improve 401 error messages: "Session expired. Please log in via the Readeck app." - Replace #if DEBUG with BuildEnvironment.isDebugBuild for runtime checks - Enable Debug Menu in both DEBUG and TestFlight builds - Replace print() statements with Logger.sync/Logger.viewModel
This commit is contained in:
parent
997d740597
commit
03cd32dd4d
@ -14,32 +14,43 @@ struct ShareBookmarkView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
ScrollViewReader { proxy in
|
if !viewModel.isConfigured || viewModel.sessionExpired {
|
||||||
ScrollView {
|
// Show setup required screen
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 24) {
|
||||||
logoSection
|
logoSection
|
||||||
serverStatusSection
|
setupRequiredSection
|
||||||
urlSection
|
Spacer()
|
||||||
tagManagementSection
|
|
||||||
.id(AddBookmarkFieldFocus.labels)
|
|
||||||
titleSection
|
|
||||||
.id(AddBookmarkFieldFocus.title)
|
|
||||||
statusSection
|
|
||||||
Spacer(minLength: 100) // Space for button
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.bottom, max(0, keyboardHeight - 120))
|
.padding()
|
||||||
.onChange(of: focusedField) { newField, _ in
|
} else {
|
||||||
guard let field = newField else { return }
|
// Normal UI
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
ScrollViewReader { proxy in
|
||||||
withAnimation(.easeInOut(duration: 0.25)) {
|
ScrollView {
|
||||||
proxy.scrollTo(field, anchor: .center)
|
VStack(spacing: 0) {
|
||||||
|
logoSection
|
||||||
|
serverStatusSection
|
||||||
|
urlSection
|
||||||
|
tagManagementSection
|
||||||
|
.id(AddBookmarkFieldFocus.labels)
|
||||||
|
titleSection
|
||||||
|
.id(AddBookmarkFieldFocus.title)
|
||||||
|
statusSection
|
||||||
|
Spacer(minLength: 100) // Space for button
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.bottom, max(0, keyboardHeight - 120))
|
||||||
|
.onChange(of: focusedField) { newField, _ in
|
||||||
|
guard let field = newField else { return }
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
|
||||||
|
withAnimation(.easeInOut(duration: 0.25)) {
|
||||||
|
proxy.scrollTo(field, anchor: .center)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
saveButtonSection
|
saveButtonSection
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.background(Color(.systemGroupedBackground))
|
.background(Color(.systemGroupedBackground))
|
||||||
.ignoresSafeArea(.keyboard, edges: .bottom)
|
.ignoresSafeArea(.keyboard, edges: .bottom)
|
||||||
@ -73,6 +84,31 @@ struct ShareBookmarkView: View {
|
|||||||
.opacity(0.9)
|
.opacity(0.9)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ViewBuilder
|
||||||
|
private var setupRequiredSection: some View {
|
||||||
|
VStack(spacing: 20) {
|
||||||
|
Image(systemName: viewModel.sessionExpired ? "person.crop.circle.badge.exclamationmark" : "exclamationmark.triangle")
|
||||||
|
.font(.system(size: 60))
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
.padding(.top, 40)
|
||||||
|
|
||||||
|
VStack(spacing: 12) {
|
||||||
|
Text(viewModel.sessionExpired ? "Session Expired" : "Setup Required")
|
||||||
|
.font(.system(size: 24, weight: .bold))
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
|
||||||
|
Text(viewModel.sessionExpired
|
||||||
|
? "Please log in via the Readeck app to continue saving bookmarks."
|
||||||
|
: "Please complete the setup in the Readeck app before using the share extension.")
|
||||||
|
.font(.system(size: 16))
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.padding(.horizontal, 32)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var serverStatusSection: some View {
|
private var serverStatusSection: some View {
|
||||||
if !viewModel.isServerReachable {
|
if !viewModel.isServerReachable {
|
||||||
|
|||||||
@ -11,6 +11,8 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
@Published var isSaving: Bool = false
|
@Published var isSaving: Bool = false
|
||||||
@Published var searchText: String = ""
|
@Published var searchText: String = ""
|
||||||
@Published var isServerReachable: Bool = true
|
@Published var isServerReachable: Bool = true
|
||||||
|
@Published var isConfigured: Bool = true
|
||||||
|
@Published var sessionExpired: Bool = false
|
||||||
let tagSortOrder: TagSortOrder = .byCount // Share Extension always uses byCount
|
let tagSortOrder: TagSortOrder = .byCount // Share Extension always uses byCount
|
||||||
let extensionContext: NSExtensionContext?
|
let extensionContext: NSExtensionContext?
|
||||||
|
|
||||||
@ -21,6 +23,13 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
init(extensionContext: NSExtensionContext?) {
|
init(extensionContext: NSExtensionContext?) {
|
||||||
self.extensionContext = extensionContext
|
self.extensionContext = extensionContext
|
||||||
logger.info("ShareBookmarkViewModel initialized with extension context: \(extensionContext != nil)")
|
logger.info("ShareBookmarkViewModel initialized with extension context: \(extensionContext != nil)")
|
||||||
|
|
||||||
|
// Check if app is configured by verifying token exists
|
||||||
|
checkConfiguration()
|
||||||
|
|
||||||
|
// Setup notification observer for 401 errors
|
||||||
|
setupNotificationObservers()
|
||||||
|
|
||||||
extractSharedContent()
|
extractSharedContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -174,6 +183,30 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
searchText = ""
|
searchText = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func checkConfiguration() {
|
||||||
|
let token = KeychainHelper.shared.loadToken()
|
||||||
|
let endpoint = KeychainHelper.shared.loadEndpoint()
|
||||||
|
|
||||||
|
if token == nil || token?.isEmpty == true || endpoint == nil || endpoint?.isEmpty == true {
|
||||||
|
logger.warning("Share extension opened but app is not configured (missing token or endpoint)")
|
||||||
|
isConfigured = false
|
||||||
|
} else {
|
||||||
|
logger.info("Share extension opened with valid configuration")
|
||||||
|
isConfigured = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func setupNotificationObservers() {
|
||||||
|
NotificationCenter.default.addObserver(
|
||||||
|
forName: .unauthorizedAPIResponse,
|
||||||
|
object: nil,
|
||||||
|
queue: .main
|
||||||
|
) { [weak self] _ in
|
||||||
|
self?.logger.warning("Received 401 Unauthorized - session expired")
|
||||||
|
self?.sessionExpired = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func completeExtensionRequest() {
|
private func completeExtensionRequest() {
|
||||||
logger.debug("Completing extension request")
|
logger.debug("Completing extension request")
|
||||||
guard let context = extensionContext else {
|
guard let context = extensionContext else {
|
||||||
@ -189,4 +222,8 @@ class ShareBookmarkViewModel: ObservableObject {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deinit {
|
||||||
|
NotificationCenter.default.removeObserver(self)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -76,6 +76,9 @@ class SimpleAPI {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: .unauthorizedAPIResponse, object: nil)
|
NotificationCenter.default.post(name: .unauthorizedAPIResponse, object: nil)
|
||||||
}
|
}
|
||||||
|
logger.error("Authentication failed: 401 Unauthorized")
|
||||||
|
showStatus("Session expired. Please log in via the Readeck app.", true)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
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)")
|
||||||
@ -129,6 +132,9 @@ class SimpleAPI {
|
|||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
NotificationCenter.default.post(name: .unauthorizedAPIResponse, object: nil)
|
NotificationCenter.default.post(name: .unauthorizedAPIResponse, object: nil)
|
||||||
}
|
}
|
||||||
|
logger.error("Authentication failed: 401 Unauthorized")
|
||||||
|
showStatus("Session expired. Please log in via the Readeck app.", true)
|
||||||
|
return 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)")
|
||||||
|
|||||||
@ -57,15 +57,15 @@ class AppViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func handleUnauthorizedResponse() async {
|
private func handleUnauthorizedResponse() async {
|
||||||
print("AppViewModel: Handling 401 Unauthorized - logging out user")
|
Logger.viewModel.info("Handling 401 Unauthorized - logging out user")
|
||||||
|
|
||||||
do {
|
do {
|
||||||
try await factory.makeLogoutUseCase().execute()
|
try await factory.makeLogoutUseCase().execute()
|
||||||
loadSetupStatus()
|
loadSetupStatus()
|
||||||
|
|
||||||
print("AppViewModel: User successfully logged out due to 401 error")
|
Logger.viewModel.info("User successfully logged out due to 401 error")
|
||||||
} catch {
|
} catch {
|
||||||
print("AppViewModel: Error during logout: \(error)")
|
Logger.viewModel.error("Error during logout: \(error.localizedDescription)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -106,22 +106,34 @@ class AppViewModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func syncTagsOnAppStart() async {
|
private func syncTagsOnAppStart() async {
|
||||||
|
// Don't sync if onboarding is not complete (no token/endpoint available)
|
||||||
|
guard settingsRepository.hasFinishedSetup else {
|
||||||
|
Logger.sync.debug("Skipping tag sync - onboarding not completed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
let now = Date()
|
let now = Date()
|
||||||
|
|
||||||
// Check if last sync was less than 2 minutes ago
|
// Check if last sync was less than 2 minutes ago
|
||||||
if let lastSync = lastAppStartTagSyncTime,
|
if let lastSync = lastAppStartTagSyncTime,
|
||||||
now.timeIntervalSince(lastSync) < 120 {
|
now.timeIntervalSince(lastSync) < 120 {
|
||||||
print("AppViewModel: Skipping tag sync - last sync was less than 2 minutes ago")
|
Logger.sync.debug("Skipping tag sync - last sync was less than 2 minutes ago")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sync tags from server to Core Data
|
// Sync tags from server to Core Data
|
||||||
print("AppViewModel: Syncing tags on app start")
|
Logger.sync.info("Syncing tags on app start")
|
||||||
try? await syncTagsUseCase.execute()
|
try? await syncTagsUseCase.execute()
|
||||||
lastAppStartTagSyncTime = now
|
lastAppStartTagSyncTime = now
|
||||||
}
|
}
|
||||||
|
|
||||||
private func syncOfflineArticlesIfNeeded() {
|
private func syncOfflineArticlesIfNeeded() {
|
||||||
|
// Don't sync if onboarding is not complete (no token/endpoint available)
|
||||||
|
guard settingsRepository.hasFinishedSetup else {
|
||||||
|
Logger.sync.debug("Skipping offline sync - onboarding not completed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// Run offline sync in background without blocking app start
|
// Run offline sync in background without blocking app start
|
||||||
Task.detached(priority: .background) { [weak self] in
|
Task.detached(priority: .background) { [weak self] in
|
||||||
guard let self = self else { return }
|
guard let self = self else { return }
|
||||||
|
|||||||
@ -249,17 +249,17 @@ struct BookmarkDetailView2: View {
|
|||||||
@ToolbarContentBuilder
|
@ToolbarContentBuilder
|
||||||
private var toolbarContent: some ToolbarContent {
|
private var toolbarContent: some ToolbarContent {
|
||||||
|
|
||||||
#if DEBUG
|
if Bundle.main.isDebugBuild {
|
||||||
// Toggle button (left)
|
// Toggle button (left)
|
||||||
ToolbarItem(placement: .navigationBarLeading) {
|
ToolbarItem(placement: .navigationBarLeading) {
|
||||||
Button(action: {
|
Button(action: {
|
||||||
useNativeWebView.toggle()
|
useNativeWebView.toggle()
|
||||||
}) {
|
}) {
|
||||||
Image(systemName: "sparkles")
|
Image(systemName: "sparkles")
|
||||||
.foregroundColor(.accentColor)
|
.foregroundColor(.accentColor)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// Top toolbar (right)
|
// Top toolbar (right)
|
||||||
ToolbarItem(placement: .navigationBarTrailing) {
|
ToolbarItem(placement: .navigationBarTrailing) {
|
||||||
|
|||||||
@ -136,38 +136,38 @@ struct OfflineReadingDetailView: View {
|
|||||||
.textCase(nil)
|
.textCase(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
if Bundle.main.isDebugBuild {
|
||||||
Section {
|
Section {
|
||||||
// Debug: Toggle offline mode simulation
|
// Debug: Toggle offline mode simulation
|
||||||
VStack(alignment: .leading, spacing: 4) {
|
VStack(alignment: .leading, spacing: 4) {
|
||||||
Toggle(isOn: Binding(
|
Toggle(isOn: Binding(
|
||||||
get: { !appSettings.isNetworkConnected },
|
get: { !appSettings.isNetworkConnected },
|
||||||
set: { isOffline in
|
set: { isOffline in
|
||||||
appSettings.isNetworkConnected = !isOffline
|
appSettings.isNetworkConnected = !isOffline
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "airplane")
|
Image(systemName: "airplane")
|
||||||
.foregroundColor(.orange)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 2) {
|
|
||||||
Text("Simulate Offline Mode".localized)
|
|
||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
Text("DEBUG: Toggle network status".localized)
|
|
||||||
.font(.caption)
|
VStack(alignment: .leading, spacing: 2) {
|
||||||
.foregroundColor(.secondary)
|
Text("Simulate Offline Mode".localized)
|
||||||
|
.foregroundColor(.orange)
|
||||||
|
Text("DEBUG: Toggle network status".localized)
|
||||||
|
.font(.caption)
|
||||||
|
.foregroundColor(.secondary)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Debug".localized)
|
||||||
|
.font(.subheadline)
|
||||||
|
.fontWeight(.semibold)
|
||||||
|
.foregroundColor(.primary)
|
||||||
|
.textCase(nil)
|
||||||
}
|
}
|
||||||
} header: {
|
|
||||||
Text("Debug".localized)
|
|
||||||
.font(.subheadline)
|
|
||||||
.fontWeight(.semibold)
|
|
||||||
.foregroundColor(.primary)
|
|
||||||
.textCase(nil)
|
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listStyle(.insetGrouped)
|
.listStyle(.insetGrouped)
|
||||||
|
|||||||
@ -86,12 +86,10 @@ struct SettingsContainerView: View {
|
|||||||
|
|
||||||
LegalPrivacySettingsView()
|
LegalPrivacySettingsView()
|
||||||
|
|
||||||
// Debug-only Logging Configuration
|
// Debug-only Settings Section
|
||||||
#if DEBUG
|
if !Bundle.main.isProduction {
|
||||||
if Bundle.main.isDebugBuild {
|
|
||||||
debugSettingsSection
|
debugSettingsSection
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
// App Info Section
|
// App Info Section
|
||||||
appInfoSection
|
appInfoSection
|
||||||
@ -104,7 +102,6 @@ struct SettingsContainerView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var debugSettingsSection: some View {
|
private var debugSettingsSection: some View {
|
||||||
Section {
|
Section {
|
||||||
@ -112,42 +109,16 @@ struct SettingsContainerView: View {
|
|||||||
icon: "wrench.and.screwdriver.fill",
|
icon: "wrench.and.screwdriver.fill",
|
||||||
iconColor: .orange,
|
iconColor: .orange,
|
||||||
title: "Debug Menu",
|
title: "Debug Menu",
|
||||||
subtitle: "Network simulation, data management & more"
|
subtitle: "Network simulation, logging & more"
|
||||||
) {
|
) {
|
||||||
DebugMenuView()
|
DebugMenuView()
|
||||||
}
|
.environmentObject(AppSettings())
|
||||||
|
|
||||||
SettingsRowNavigationLink(
|
|
||||||
icon: "list.bullet.rectangle",
|
|
||||||
iconColor: .blue,
|
|
||||||
title: "Debug Logs",
|
|
||||||
subtitle: "View all debug messages"
|
|
||||||
) {
|
|
||||||
DebugLogViewer()
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsRowNavigationLink(
|
|
||||||
icon: "slider.horizontal.3",
|
|
||||||
iconColor: .purple,
|
|
||||||
title: "Logging Configuration",
|
|
||||||
subtitle: "Configure log levels and categories"
|
|
||||||
) {
|
|
||||||
LoggingConfigurationView()
|
|
||||||
}
|
|
||||||
|
|
||||||
SettingsRowNavigationLink(
|
|
||||||
icon: "textformat",
|
|
||||||
iconColor: .green,
|
|
||||||
title: "Font Debug",
|
|
||||||
subtitle: "View available fonts"
|
|
||||||
) {
|
|
||||||
FontDebugView()
|
|
||||||
}
|
}
|
||||||
} header: {
|
} header: {
|
||||||
HStack {
|
HStack {
|
||||||
Text("Debug Settings")
|
Text("Debug Settings")
|
||||||
Spacer()
|
Spacer()
|
||||||
Text("DEBUG BUILD")
|
Text(Bundle.main.isTestFlightBuild ? "TESTFLIGHT" : "DEBUG BUILD")
|
||||||
.font(.caption2)
|
.font(.caption2)
|
||||||
.padding(.horizontal, 6)
|
.padding(.horizontal, 6)
|
||||||
.padding(.vertical, 2)
|
.padding(.vertical, 2)
|
||||||
@ -155,9 +126,10 @@ struct SettingsContainerView: View {
|
|||||||
.foregroundColor(.orange)
|
.foregroundColor(.orange)
|
||||||
.clipShape(Capsule())
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
|
} footer: {
|
||||||
|
Text("Debug menu is also accessible via shake gesture")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var appInfoSection: some View {
|
private var appInfoSection: some View {
|
||||||
|
|||||||
@ -45,35 +45,35 @@ struct SettingsGeneralView: View {
|
|||||||
Text("Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly.")
|
Text("Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly.")
|
||||||
}
|
}
|
||||||
|
|
||||||
#if DEBUG
|
if Bundle.main.isDebugBuild {
|
||||||
Section {
|
Section {
|
||||||
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
|
||||||
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
|
||||||
} header: {
|
} header: {
|
||||||
Text("Reading Settings")
|
Text("Reading Settings")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let successMessage = viewModel.successMessage {
|
if let successMessage = viewModel.successMessage {
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
Text(successMessage)
|
Text(successMessage)
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let errorMessage = viewModel.errorMessage {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(errorMessage)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let errorMessage = viewModel.errorMessage {
|
|
||||||
Section {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
Text(errorMessage)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $showReleaseNotes) {
|
.sheet(isPresented: $showReleaseNotes) {
|
||||||
ReleaseNotesView()
|
ReleaseNotesView()
|
||||||
|
|||||||
@ -16,37 +16,37 @@ struct SyncSettingsView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Group {
|
Group {
|
||||||
#if DEBUG
|
if Bundle.main.isDebugBuild {
|
||||||
Section {
|
Section {
|
||||||
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
|
Toggle("Automatic sync", isOn: $viewModel.autoSyncEnabled)
|
||||||
if viewModel.autoSyncEnabled {
|
if viewModel.autoSyncEnabled {
|
||||||
Stepper("Sync interval: \(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60)
|
Stepper("Sync interval: \(viewModel.syncInterval) minutes", value: $viewModel.syncInterval, in: 1...60)
|
||||||
|
}
|
||||||
|
} header: {
|
||||||
|
Text("Sync Settings")
|
||||||
}
|
}
|
||||||
} header: {
|
|
||||||
Text("Sync Settings")
|
|
||||||
}
|
|
||||||
|
|
||||||
if let successMessage = viewModel.successMessage {
|
if let successMessage = viewModel.successMessage {
|
||||||
Section {
|
Section {
|
||||||
HStack {
|
HStack {
|
||||||
Image(systemName: "checkmark.circle.fill")
|
Image(systemName: "checkmark.circle.fill")
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
Text(successMessage)
|
Text(successMessage)
|
||||||
.foregroundColor(.green)
|
.foregroundColor(.green)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let errorMessage = viewModel.errorMessage {
|
||||||
|
Section {
|
||||||
|
HStack {
|
||||||
|
Image(systemName: "exclamationmark.triangle.fill")
|
||||||
|
.foregroundColor(.red)
|
||||||
|
Text(errorMessage)
|
||||||
|
.foregroundColor(.red)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if let errorMessage = viewModel.errorMessage {
|
|
||||||
Section {
|
|
||||||
HStack {
|
|
||||||
Image(systemName: "exclamationmark.triangle.fill")
|
|
||||||
.foregroundColor(.red)
|
|
||||||
Text(errorMessage)
|
|
||||||
.foregroundColor(.red)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
.task {
|
.task {
|
||||||
await viewModel.loadGeneralSettings()
|
await viewModel.loadGeneralSettings()
|
||||||
|
|||||||
@ -54,17 +54,17 @@ class LogConfiguration: ObservableObject {
|
|||||||
|
|
||||||
private init() {
|
private init() {
|
||||||
// First time setup: Enable logging in DEBUG builds with sensible defaults
|
// First time setup: Enable logging in DEBUG builds with sensible defaults
|
||||||
#if DEBUG
|
if Bundle.main.isDebugBuild {
|
||||||
if UserDefaults.standard.object(forKey: "LogConfigurationInitialized") == nil {
|
if UserDefaults.standard.object(forKey: "LogConfigurationInitialized") == nil {
|
||||||
isLoggingEnabled = true
|
isLoggingEnabled = true
|
||||||
showPerformanceLogs = true
|
showPerformanceLogs = true
|
||||||
showTimestamps = true
|
showTimestamps = true
|
||||||
includeSourceLocation = true
|
includeSourceLocation = true
|
||||||
globalMinLevel = .debug
|
globalMinLevel = .debug
|
||||||
UserDefaults.standard.set(true, forKey: "LogConfigurationInitialized")
|
UserDefaults.standard.set(true, forKey: "LogConfigurationInitialized")
|
||||||
saveConfiguration()
|
saveConfiguration()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
|
|
||||||
loadConfiguration()
|
loadConfiguration()
|
||||||
}
|
}
|
||||||
@ -184,7 +184,7 @@ struct Logger {
|
|||||||
// MARK: - Store Log
|
// MARK: - Store Log
|
||||||
|
|
||||||
private func storeLog(message: String, level: LogLevel, file: String, function: String, line: Int) {
|
private func storeLog(message: String, level: LogLevel, file: String, function: String, line: Int) {
|
||||||
#if DEBUG
|
guard Bundle.main.isDebugBuild else { return }
|
||||||
guard config.isLoggingEnabled else { return }
|
guard config.isLoggingEnabled else { return }
|
||||||
let entry = LogEntry(
|
let entry = LogEntry(
|
||||||
level: level,
|
level: level,
|
||||||
@ -197,7 +197,6 @@ struct Logger {
|
|||||||
Task {
|
Task {
|
||||||
await LogStore.shared.addEntry(entry)
|
await LogStore.shared.addEntry(entry)
|
||||||
}
|
}
|
||||||
#endif
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - Convenience Methods
|
// MARK: - Convenience Methods
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user