Merge branch 'develop' of https://codeberg.org/readeck/readeck-ios into develop

This commit is contained in:
Ilyas Hallak 2025-09-26 21:58:54 +02:00
commit f3147a6cc6
13 changed files with 87 additions and 17 deletions

3
.gitignore vendored
View File

@ -66,3 +66,6 @@ fastlane/AuthKey_JZJCQWW9N3.p8
# Documentation # Documentation
documentation/ documentation/
# macOS
**/.DS_Store

View File

@ -14,6 +14,8 @@ struct Settings {
var theme: Theme? = nil var theme: Theme? = nil
var cardLayoutStyle: CardLayoutStyle? = nil var cardLayoutStyle: CardLayoutStyle? = nil
var urlOpener: UrlOpener? = nil
var isLoggedIn: Bool { var isLoggedIn: Bool {
token != nil && !token!.isEmpty token != nil && !token!.isEmpty
} }
@ -91,6 +93,10 @@ class SettingsRepository: PSettingsRepository {
existingSettings.theme = theme.rawValue existingSettings.theme = theme.rawValue
} }
if let urlOpener = settings.urlOpener {
existingSettings.urlOpener = urlOpener.rawValue
}
if let cardLayoutStyle = settings.cardLayoutStyle { if let cardLayoutStyle = settings.cardLayoutStyle {
existingSettings.cardLayoutStyle = cardLayoutStyle.rawValue existingSettings.cardLayoutStyle = cardLayoutStyle.rawValue
} }
@ -132,7 +138,8 @@ class SettingsRepository: PSettingsRepository {
fontSize: FontSize(rawValue: settingEntity?.fontSize ?? FontSize.medium.rawValue), fontSize: FontSize(rawValue: settingEntity?.fontSize ?? FontSize.medium.rawValue),
enableTTS: settingEntity?.enableTTS, enableTTS: settingEntity?.enableTTS,
theme: Theme(rawValue: settingEntity?.theme ?? Theme.system.rawValue), theme: Theme(rawValue: settingEntity?.theme ?? Theme.system.rawValue),
cardLayoutStyle: CardLayoutStyle(rawValue: settingEntity?.cardLayoutStyle ?? CardLayoutStyle.magazine.rawValue) cardLayoutStyle: CardLayoutStyle(rawValue: settingEntity?.cardLayoutStyle ?? CardLayoutStyle.magazine.rawValue),
urlOpener: UrlOpener(rawValue: settingEntity?.urlOpener ?? UrlOpener.inAppBrowser.rawValue)
) )
continuation.resume(returning: settings) continuation.resume(returning: settings)
} catch { } catch {

View File

@ -0,0 +1,11 @@
enum UrlOpener: String, CaseIterable {
case inAppBrowser = "inAppBrowser"
case defaultBrowser = "defaultBrowser"
var displayName: String {
switch self {
case .inAppBrowser: return "In App Browser"
case .defaultBrowser: return "Default Browser"
}
}
}

View File

@ -4,6 +4,7 @@ protocol PSaveSettingsUseCase {
func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws
func execute(enableTTS: Bool) async throws func execute(enableTTS: Bool) async throws
func execute(theme: Theme) async throws func execute(theme: Theme) async throws
func execute(urlOpener: UrlOpener) async throws
} }
class SaveSettingsUseCase: PSaveSettingsUseCase { class SaveSettingsUseCase: PSaveSettingsUseCase {
@ -33,4 +34,10 @@ class SaveSettingsUseCase: PSaveSettingsUseCase {
.init(theme: theme) .init(theme: theme)
) )
} }
func execute(urlOpener: UrlOpener) async throws {
try await settingsRepository.saveSettings(
.init(urlOpener: urlOpener)
)
}
} }

View File

@ -46,6 +46,9 @@
"General Settings" = "Allgemeine Einstellungen"; "General Settings" = "Allgemeine Einstellungen";
"Server Settings" = "Server-Einstellungen"; "Server Settings" = "Server-Einstellungen";
"Server Connection" = "Server-Verbindung"; "Server Connection" = "Server-Verbindung";
"Open external links in" = "Öffne externe Links in";
"In App Browser" = "In App Browser";
"Default Browser" = "Standard Browser";
"Add" = "Hinzufügen"; "Add" = "Hinzufügen";
"Add new tag:" = "Neues Label hinzufügen:"; "Add new tag:" = "Neues Label hinzufügen:";

View File

@ -72,7 +72,7 @@ struct BookmarkDetailView: View {
.padding() .padding()
} else { } else {
Button(action: { Button(action: {
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url) URLUtil.open(url: viewModel.bookmarkDetail.url, urlOpener: appSettings.urlOpener)
}) { }) {
HStack { HStack {
Image(systemName: "safari") Image(systemName: "safari")
@ -263,7 +263,7 @@ struct BookmarkDetailView: View {
.padding() .padding()
} else { } else {
Button(action: { Button(action: {
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url) URLUtil.open(url: viewModel.bookmarkDetail.url, urlOpener: appSettings.urlOpener)
}) { }) {
HStack { HStack {
Image(systemName: "safari") Image(systemName: "safari")
@ -319,7 +319,7 @@ struct BookmarkDetailView: View {
metaRow(icon: "safari") { metaRow(icon: "safari") {
Button(action: { Button(action: {
SafariUtil.openInSafari(url: viewModel.bookmarkDetail.url) URLUtil.open(url: viewModel.bookmarkDetail.url, urlOpener: appSettings.urlOpener)
}) { }) {
Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open") Text((URLUtil.extractDomain(from: viewModel.bookmarkDetail.url) ?? "Open original page") + " open")
.font(.subheadline) .font(.subheadline)

View File

@ -14,6 +14,7 @@ extension View {
struct BookmarkCardView: View { struct BookmarkCardView: View {
@Environment(\.colorScheme) var colorScheme @Environment(\.colorScheme) var colorScheme
@EnvironmentObject var appSettings: AppSettings
let bookmark: Bookmark let bookmark: Bookmark
let currentState: BookmarkState let currentState: BookmarkState
@ -255,7 +256,7 @@ struct BookmarkCardView: View {
HStack { HStack {
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari") Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari")
.onTapGesture { .onTapGesture {
SafariUtil.openInSafari(url: bookmark.url) URLUtil.open(url: bookmark.url, urlOpener: appSettings.urlOpener)
} }
} }
} }
@ -336,7 +337,7 @@ struct BookmarkCardView: View {
HStack { HStack {
Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari") Label((URLUtil.extractDomain(from: bookmark.url) ?? "Original Site") + " open", systemImage: "safari")
.onTapGesture { .onTapGesture {
SafariUtil.openInSafari(url: bookmark.url) URLUtil.open(url: bookmark.url, urlOpener: appSettings.urlOpener)
} }
} }
} }

View File

@ -150,6 +150,7 @@ class MockSaveSettingsUseCase: PSaveSettingsUseCase {
func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws {} func execute(selectedFontFamily: FontFamily, selectedFontSize: FontSize) async throws {}
func execute(enableTTS: Bool) async throws {} func execute(enableTTS: Bool) async throws {}
func execute(theme: Theme) async throws {} func execute(theme: Theme) async throws {}
func execute(urlOpener: UrlOpener) async throws {}
} }
class MockGetBookmarkUseCase: PGetBookmarkUseCase { class MockGetBookmarkUseCase: PGetBookmarkUseCase {

View File

@ -27,6 +27,10 @@ class AppSettings: ObservableObject {
settings?.theme ?? .system settings?.theme ?? .system
} }
var urlOpener: UrlOpener {
settings?.urlOpener ?? .inAppBrowser
}
init(settings: Settings? = nil) { init(settings: Settings? = nil) {
self.settings = settings self.settings = settings
} }

View File

@ -33,6 +33,23 @@ struct SettingsGeneralView: View {
.font(.footnote) .font(.footnote)
} }
// Reading Settings
VStack(alignment: .leading, spacing: 12) {
Text("Open external links in".localized)
.font(.headline)
Picker("urlOpener", selection: $viewModel.urlOpener) {
ForEach(UrlOpener.allCases, id: \.self) { urlOpener in
Text(urlOpener.displayName.localized).tag(urlOpener)
}
}
.pickerStyle(.segmented)
.onChange(of: viewModel.urlOpener) {
Task {
await viewModel.saveGeneralSettings()
}
}
}
#if DEBUG #if DEBUG
// Sync Settings // Sync Settings
VStack(alignment: .leading, spacing: 12) { VStack(alignment: .leading, spacing: 12) {
@ -55,8 +72,6 @@ struct SettingsGeneralView: View {
.font(.headline) .font(.headline)
Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode) Toggle("Safari Reader Mode", isOn: $viewModel.enableReaderMode)
.toggleStyle(SwitchToggleStyle()) .toggleStyle(SwitchToggleStyle())
Toggle("Open external links in in-app Safari", isOn: $viewModel.openExternalLinksInApp)
.toggleStyle(SwitchToggleStyle())
Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead) Toggle("Automatically mark articles as read", isOn: $viewModel.autoMarkAsRead)
.toggleStyle(SwitchToggleStyle()) .toggleStyle(SwitchToggleStyle())
} }

View File

@ -15,8 +15,8 @@ class SettingsGeneralViewModel {
// MARK: - Reading Settings // MARK: - Reading Settings
var enableReaderMode: Bool = false var enableReaderMode: Bool = false
var enableTTS: Bool = false var enableTTS: Bool = false
var openExternalLinksInApp: Bool = true
var autoMarkAsRead: Bool = false var autoMarkAsRead: Bool = false
var urlOpener: UrlOpener = .inAppBrowser
// MARK: - Messages // MARK: - Messages
@ -36,6 +36,7 @@ class SettingsGeneralViewModel {
if let settings = try await loadSettingsUseCase.execute() { if let settings = try await loadSettingsUseCase.execute() {
enableTTS = settings.enableTTS ?? false enableTTS = settings.enableTTS ?? false
selectedTheme = settings.theme ?? .system selectedTheme = settings.theme ?? .system
urlOpener = settings.urlOpener ?? .inAppBrowser
autoSyncEnabled = false autoSyncEnabled = false
} }
} catch { } catch {
@ -48,6 +49,7 @@ class SettingsGeneralViewModel {
do { do {
try await saveSettingsUseCase.execute(enableTTS: enableTTS) try await saveSettingsUseCase.execute(enableTTS: enableTTS)
try await saveSettingsUseCase.execute(theme: selectedTheme) try await saveSettingsUseCase.execute(theme: selectedTheme)
try await saveSettingsUseCase.execute(urlOpener: urlOpener)
successMessage = "Settings saved" successMessage = "Settings saved"

View File

@ -1,8 +1,25 @@
import UIKit import UIKit
import SafariServices import SafariServices
class SafariUtil { struct URLUtil {
static func openInSafari(url: String) {
static func open(url: String, urlOpener: UrlOpener = .inAppBrowser) {
// Could be extended to open in other browsers like Firefox, Brave etc. if somebody has a multi browser setup
// and wants readeck links to always opened in a specific browser
switch urlOpener {
case .defaultBrowser:
openUrlInDefaultBrowser(url: url)
default:
openUrlInInAppBrowser(url: url)
}
}
static func openUrlInDefaultBrowser(url: String) {
guard let url = URL(string: url) else { return }
UIApplication.shared.open(url, options: [:], completionHandler: nil)
}
static func openUrlInInAppBrowser(url: String) {
guard let url = URL(string: url) else { return } guard let url = URL(string: url) else { return }
if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
@ -22,9 +39,7 @@ class SafariUtil {
presentingViewController.present(safariViewController, animated: true) presentingViewController.present(safariViewController, animated: true)
} }
} }
}
struct URLUtil {
static func extractDomain(from urlString: String) -> String? { static func extractDomain(from urlString: String) -> String? {
guard let url = URL(string: urlString), let host = url.host else { return nil } guard let url = URL(string: urlString), let host = url.host else { return nil }
return host.replacingOccurrences(of: "www.", with: "") return host.replacingOccurrences(of: "www.", with: "")

View File

@ -1,5 +1,5 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes"?> <?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="23788.4" systemVersion="24G84" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier=""> <model type="com.apple.IDECoreDataModeler.DataModel" documentVersion="1.0" lastSavedToolsVersion="24299" systemVersion="25A354" minimumToolsVersion="Automatic" sourceLanguage="Swift" userDefinedModelVersionIdentifier="">
<entity name="ArticleURLEntity" representedClassName="ArticleURLEntity" syncable="YES" codeGenerationType="class"> <entity name="ArticleURLEntity" representedClassName="ArticleURLEntity" syncable="YES" codeGenerationType="class">
<attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/> <attribute name="id" optional="YES" attributeType="UUID" usesScalarValueType="NO"/>
<attribute name="tags" optional="YES" attributeType="String"/> <attribute name="tags" optional="YES" attributeType="String"/>
@ -57,6 +57,7 @@
<attribute name="fontSize" optional="YES" attributeType="String"/> <attribute name="fontSize" optional="YES" attributeType="String"/>
<attribute name="theme" optional="YES" attributeType="String"/> <attribute name="theme" optional="YES" attributeType="String"/>
<attribute name="token" optional="YES" attributeType="String"/> <attribute name="token" optional="YES" attributeType="String"/>
<attribute name="urlOpener" optional="YES" attributeType="String"/>
</entity> </entity>
<entity name="TagEntity" representedClassName="TagEntity" syncable="YES" codeGenerationType="class"> <entity name="TagEntity" representedClassName="TagEntity" syncable="YES" codeGenerationType="class">
<attribute name="name" optional="YES" attributeType="String"/> <attribute name="name" optional="YES" attributeType="String"/>