feat: Add comprehensive i18n support and Legal & Privacy section
- Create String+Localization extension with .localized property - Add LabelUtils for consistent label splitting and deduplication logic - Implement Legal & Privacy settings section with Privacy Policy and Legal Notice views - Add German/English localization for all navigation states and settings sections - Fix navigationDestination placement warning in PadSidebarView - Unify label input handling across main app and share extension - Support for space-separated label input in share extension Navigation & Settings now fully localized: - All/Unread/Favorites/Archive → Alle/Ungelesen/Favoriten/Archiv - Font/Appearance/Cache/General/Server Settings → German equivalents - Legal section with GitHub issue reporting and email support contact
This commit is contained in:
parent
534ceddad4
commit
d6ea56cfa9
@ -208,19 +208,15 @@ struct ShareBookmarkView: View {
|
||||
}
|
||||
|
||||
private func addCustomTag() {
|
||||
let trimmed = viewModel.searchText.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else { return }
|
||||
let splitLabels = LabelUtils.splitLabelsFromInput(viewModel.searchText)
|
||||
let availableLabels = viewModel.labels.map { $0.name }
|
||||
let currentLabels = Array(viewModel.selectedLabels)
|
||||
let uniqueLabels = LabelUtils.filterUniqueLabels(splitLabels, currentLabels: currentLabels, availableLabels: availableLabels)
|
||||
|
||||
let lowercased = trimmed.lowercased()
|
||||
let allExisting = Set(viewModel.labels.map { $0.name.lowercased() })
|
||||
let allSelected = Set(viewModel.selectedLabels.map { $0.lowercased() })
|
||||
|
||||
if allExisting.contains(lowercased) || allSelected.contains(lowercased) {
|
||||
// Tag already exists, don't add
|
||||
return
|
||||
} else {
|
||||
viewModel.selectedLabels.insert(trimmed)
|
||||
viewModel.searchText = ""
|
||||
for label in uniqueLabels {
|
||||
viewModel.selectedLabels.insert(label)
|
||||
}
|
||||
|
||||
viewModel.searchText = ""
|
||||
}
|
||||
}
|
||||
|
||||
13
readeck/Data/Extensions/String+Localization.swift
Normal file
13
readeck/Data/Extensions/String+Localization.swift
Normal file
@ -0,0 +1,13 @@
|
||||
import Foundation
|
||||
|
||||
extension String {
|
||||
/// Returns a localized version of the string using NSLocalizedString
|
||||
var localized: String {
|
||||
return NSLocalizedString(self, comment: "")
|
||||
}
|
||||
|
||||
/// Returns a localized version of the string with comment
|
||||
func localized(comment: String) -> String {
|
||||
return NSLocalizedString(self, comment: comment)
|
||||
}
|
||||
}
|
||||
29
readeck/Data/Utils/LabelUtils.swift
Normal file
29
readeck/Data/Utils/LabelUtils.swift
Normal file
@ -0,0 +1,29 @@
|
||||
import Foundation
|
||||
|
||||
struct LabelUtils {
|
||||
/// Splits a label input string by spaces and returns individual trimmed labels
|
||||
/// - Parameter input: The input string containing one or more labels separated by spaces
|
||||
/// - Returns: Array of individual trimmed labels, excluding empty strings
|
||||
static func splitLabelsFromInput(_ input: String) -> [String] {
|
||||
return input
|
||||
.components(separatedBy: " ")
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
}
|
||||
|
||||
/// Filters out labels that already exist in current or available labels
|
||||
/// - Parameters:
|
||||
/// - labels: Array of labels to filter
|
||||
/// - currentLabels: Currently selected labels
|
||||
/// - availableLabels: Available labels (optional)
|
||||
/// - Returns: Array of unique labels that don't already exist
|
||||
static func filterUniqueLabels(_ labels: [String], currentLabels: [String], availableLabels: [String] = []) -> [String] {
|
||||
let currentSet = Set(currentLabels.map { $0.lowercased() })
|
||||
let availableSet = Set(availableLabels.map { $0.lowercased() })
|
||||
|
||||
return labels.filter { label in
|
||||
let lowercased = label.lowercased()
|
||||
return !currentSet.contains(lowercased) && !availableSet.contains(lowercased)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -18,6 +18,34 @@
|
||||
"%lld/%lld" = "%1$lld/%2$lld";
|
||||
"12 min • Today • example.com" = "12 min • Today • example.com";
|
||||
"Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly." = "Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly.";
|
||||
|
||||
/* Legal & Privacy */
|
||||
"Legal & Privacy" = "Legal & Privacy";
|
||||
"Privacy Policy" = "Privacy Policy";
|
||||
"Legal Notice" = "Legal Notice";
|
||||
"Report an Issue" = "Report an Issue";
|
||||
"Contact Support" = "Contact Support";
|
||||
|
||||
/* Navigation & States */
|
||||
"All" = "All";
|
||||
"Unread" = "Unread";
|
||||
"Favorites" = "Favorites";
|
||||
"Archive" = "Archive";
|
||||
"Search" = "Search";
|
||||
"Settings" = "Settings";
|
||||
"Articles" = "Articles";
|
||||
"Videos" = "Videos";
|
||||
"Pictures" = "Pictures";
|
||||
"Tags" = "Tags";
|
||||
|
||||
/* Settings Sections */
|
||||
"Font Settings" = "Font Settings";
|
||||
"Appearance" = "Appearance";
|
||||
"Cache Settings" = "Cache Settings";
|
||||
"General Settings" = "General Settings";
|
||||
"Server Settings" = "Server Settings";
|
||||
"Server Connection" = "Server Connection";
|
||||
|
||||
"Add" = "Add";
|
||||
"Add new tag:" = "Add new tag:";
|
||||
"all" = "all";
|
||||
|
||||
@ -5,4 +5,153 @@
|
||||
Created by conversion from Localizable.xcstrings
|
||||
*/
|
||||
|
||||
"all" = "Ale";
|
||||
|
||||
"" = "";
|
||||
"(%lld found)" = "(%lld gefunden)";
|
||||
"%" = "%";
|
||||
"%@ (%lld)" = "%1$@ (%2$lld)";
|
||||
"%lld" = "%lld";
|
||||
"%lld articles in the queue" = "%lld Artikel in der Warteschlange";
|
||||
"%lld bookmark%@ synced successfully" = "%1$lld Lesezeichen%2$@ erfolgreich synchronisiert";
|
||||
"%lld bookmark%@ waiting for sync" = "%1$lld Lesezeichen%2$@ warten auf Synchronisation";
|
||||
"%lld min" = "%lld Min";
|
||||
"%lld." = "%lld.";
|
||||
"%lld/%lld" = "%1$lld/%2$lld";
|
||||
"12 min • Today • example.com" = "12 Min • Heute • example.com";
|
||||
"Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly." = "Aktiviere die Vorlese-Funktion, um deine Artikel vorlesen zu lassen. Dies ist eine sehr frühe Vorschau und funktioniert möglicherweise noch nicht perfekt.";
|
||||
|
||||
/* Legal & Privacy */
|
||||
"Legal & Privacy" = "Rechtliches & Datenschutz";
|
||||
"Privacy Policy" = "Datenschutzerklärung";
|
||||
"Legal Notice" = "Impressum";
|
||||
"Report an Issue" = "Problem melden";
|
||||
"Contact Support" = "Support kontaktieren";
|
||||
|
||||
/* Navigation & States */
|
||||
"All" = "Alle";
|
||||
"Unread" = "Ungelesen";
|
||||
"Favorites" = "Favoriten";
|
||||
"Archive" = "Archiv";
|
||||
"Search" = "Suchen";
|
||||
"Settings" = "Einstellungen";
|
||||
"Articles" = "Artikel";
|
||||
"Videos" = "Videos";
|
||||
"Pictures" = "Bilder";
|
||||
"Tags" = "Labels";
|
||||
|
||||
/* Settings Sections */
|
||||
"Font Settings" = "Schriftart-Einstellungen";
|
||||
"Appearance" = "Darstellung";
|
||||
"Cache Settings" = "Cache-Einstellungen";
|
||||
"General Settings" = "Allgemeine Einstellungen";
|
||||
"Server Settings" = "Server-Einstellungen";
|
||||
"Server Connection" = "Server-Verbindung";
|
||||
|
||||
"Add" = "Hinzufügen";
|
||||
"Add new tag:" = "Neues Label hinzufügen:";
|
||||
"all" = "alle";
|
||||
"All tags selected" = "Alle Labels ausgewählt";
|
||||
"Archive" = "Archivieren";
|
||||
"Archive bookmark" = "Lesezeichen archivieren";
|
||||
"Are you sure you want to delete this bookmark? This action cannot be undone." = "Dieses Lesezeichen wirklich löschen? Diese Aktion kann nicht rückgängig gemacht werden.";
|
||||
"Are you sure you want to log out? This will delete all your login credentials and return you to setup." = "Wirklich abmelden? Dies löscht alle Anmeldedaten und führt zurück zur Einrichtung.";
|
||||
"Available tags" = "Verfügbare Labels";
|
||||
"Cancel" = "Abbrechen";
|
||||
"Category-specific Levels" = "Kategorie-spezifische Level";
|
||||
"Changes take effect immediately. Lower log levels include higher ones (Debug includes all, Critical includes only critical messages)." = "Änderungen werden sofort wirksam. Niedrigere Log-Level enthalten höhere (Debug enthält alle, Critical nur kritische Nachrichten).";
|
||||
"Close" = "Schließen";
|
||||
"Configure log levels and categories" = "Log-Level und Kategorien konfigurieren";
|
||||
"Critical" = "Kritisch";
|
||||
"Debug" = "Debug";
|
||||
"DEBUG BUILD" = "DEBUG BUILD";
|
||||
"Debug Settings" = "Debug-Einstellungen";
|
||||
"Delete" = "Löschen";
|
||||
"Delete Bookmark" = "Lesezeichen löschen";
|
||||
"Developer: Ilyas Hallak" = "Entwickler: Ilyas Hallak";
|
||||
"Done" = "Fertig";
|
||||
"Enter an optional title..." = "Optionalen Titel eingeben...";
|
||||
"Enter your Readeck server details to get started." = "Readeck-Server-Details eingeben, um zu beginnen.";
|
||||
"Error" = "Fehler";
|
||||
"Error: %@" = "Fehler: %@";
|
||||
"Favorite" = "Favorit";
|
||||
"Finished reading?" = "Fertig gelesen?";
|
||||
"Font" = "Schrift";
|
||||
"Font family" = "Schriftart";
|
||||
"Font Settings" = "Schrift-Einstellungen";
|
||||
"Font size" = "Schriftgröße";
|
||||
"From Bremen with 💚" = "Aus Bremen mit 💚";
|
||||
"General" = "Allgemein";
|
||||
"Global Level" = "Globales Level";
|
||||
"Global Minimum Level" = "Globales Minimum-Level";
|
||||
"Global Settings" = "Globale Einstellungen";
|
||||
"https://example.com" = "https://example.com";
|
||||
"https://readeck.example.com" = "https://readeck.example.com";
|
||||
"Include Source Location" = "Quellort einschließen";
|
||||
"Info" = "Info";
|
||||
"Jump to last read position (%lld%%)" = "Zur letzten Leseposition springen (%lld%%)";
|
||||
"Key" = "Schlüssel";
|
||||
"Level for %@" = "Level für %@";
|
||||
"Loading %@" = "Lade %@";
|
||||
"Loading article..." = "Artikel wird geladen...";
|
||||
"Logging Configuration" = "Logging-Konfiguration";
|
||||
"Login & Save" = "Anmelden & Speichern";
|
||||
"Logout" = "Abmelden";
|
||||
"Logs below this level will be filtered out globally" = "Logs unter diesem Level werden global herausgefiltert";
|
||||
"Manage Labels" = "Labels verwalten";
|
||||
"Mark as favorite" = "Als Favorit markieren";
|
||||
"More" = "Mehr";
|
||||
"New Bookmark" = "Neues Lesezeichen";
|
||||
"No articles in the queue" = "Keine Artikel in der Warteschlange";
|
||||
"No bookmarks" = "Keine Lesezeichen";
|
||||
"No bookmarks found in %@." = "Keine Lesezeichen in %@ gefunden.";
|
||||
"No bookmarks found." = "Keine Lesezeichen gefunden.";
|
||||
"No results" = "Keine Ergebnisse";
|
||||
"Notice" = "Hinweis";
|
||||
"OK" = "OK";
|
||||
"Optional: Custom title" = "Optional: Benutzerdefinierter Titel";
|
||||
"Password" = "Passwort";
|
||||
"Paste" = "Einfügen";
|
||||
"Please wait while we fetch your bookmarks..." = "Bitte warten, während die Lesezeichen geladen werden...";
|
||||
"Preview" = "Vorschau";
|
||||
"Progress: %lld%%" = "Fortschritt: %lld%%";
|
||||
"Re-login & Save" = "Erneut anmelden & Speichern";
|
||||
"Read Aloud Feature" = "Vorlese-Funktion";
|
||||
"Read article aloud" = "Artikel vorlesen";
|
||||
"Read-aloud Queue" = "Vorlese-Warteschlange";
|
||||
"readeck Bookmark Title" = "readeck Lesezeichen-Titel";
|
||||
"Reading %lld/%lld: " = "Lese %1$lld/%2$lld: ";
|
||||
"Remove" = "Entfernen";
|
||||
"Reset" = "Zurücksetzen";
|
||||
"Reset to Defaults" = "Auf Standardwerte zurücksetzen";
|
||||
"Restore" = "Wiederherstellen";
|
||||
"Resume listening" = "Zuhören fortsetzen";
|
||||
"Save bookmark" = "Lesezeichen speichern";
|
||||
"Save Bookmark" = "Lesezeichen speichern";
|
||||
"Saving..." = "Speichern...";
|
||||
"Search" = "Suchen";
|
||||
"Search or add new tag..." = "Suchen oder neues Label hinzufügen...";
|
||||
"Search results" = "Suchergebnisse";
|
||||
"Search..." = "Suchen...";
|
||||
"Searching..." = "Suche...";
|
||||
"Select a bookmark or tag" = "Lesezeichen oder Label auswählen";
|
||||
"Selected tags" = "Ausgewählte Labels";
|
||||
"Server Endpoint" = "Server-Endpunkt";
|
||||
"Server not reachable - saving locally" = "Server nicht erreichbar - speichere lokal";
|
||||
"Settings" = "Einstellungen";
|
||||
"Show Performance Logs" = "Performance-Logs anzeigen";
|
||||
"Show Timestamps" = "Zeitstempel anzeigen";
|
||||
"Speed" = "Geschwindigkeit";
|
||||
"Syncing with server..." = "Synchronisiere mit Server...";
|
||||
"Theme" = "Design";
|
||||
"This is how your bookmark descriptions and article text will appear in the app. The quick brown fox jumps over the lazy dog." = "So werden Lesezeichen-Beschreibungen und Artikeltexte in der App angezeigt. Franz jagt im komplett verwahrlosten Taxi quer durch Bayern.";
|
||||
"Try Again" = "Erneut versuchen";
|
||||
"Unable to load bookmarks" = "Lesezeichen können nicht geladen werden";
|
||||
"Unarchive Bookmark" = "Lesezeichen aus Archiv entfernen";
|
||||
"URL in clipboard:" = "URL in Zwischenablage:";
|
||||
"Username" = "Benutzername";
|
||||
"Version %@" = "Version %@";
|
||||
"Warning" = "Warnung";
|
||||
"Your current server connection and login credentials." = "Aktuelle Serververbindung und Anmeldedaten.";
|
||||
"Your Password" = "Passwort";
|
||||
"Your Username" = "Benutzername";
|
||||
|
||||
|
||||
@ -18,6 +18,34 @@
|
||||
"%lld/%lld" = "%1$lld/%2$lld";
|
||||
"12 min • Today • example.com" = "12 min • Today • example.com";
|
||||
"Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly." = "Activate the Read Aloud Feature to read aloud your articles. This is a really early preview and might not work perfectly.";
|
||||
|
||||
/* Legal & Privacy */
|
||||
"Legal & Privacy" = "Legal & Privacy";
|
||||
"Privacy Policy" = "Privacy Policy";
|
||||
"Legal Notice" = "Legal Notice";
|
||||
"Report an Issue" = "Report an Issue";
|
||||
"Contact Support" = "Contact Support";
|
||||
|
||||
/* Navigation & States */
|
||||
"All" = "All";
|
||||
"Unread" = "Unread";
|
||||
"Favorites" = "Favorites";
|
||||
"Archive" = "Archive";
|
||||
"Search" = "Search";
|
||||
"Settings" = "Settings";
|
||||
"Articles" = "Articles";
|
||||
"Videos" = "Videos";
|
||||
"Pictures" = "Pictures";
|
||||
"Tags" = "Tags";
|
||||
|
||||
/* Settings Sections */
|
||||
"Font Settings" = "Font Settings";
|
||||
"Appearance" = "Appearance";
|
||||
"Cache Settings" = "Cache Settings";
|
||||
"General Settings" = "General Settings";
|
||||
"Server Settings" = "Server Settings";
|
||||
"Server Connection" = "Server Connection";
|
||||
|
||||
"Add" = "Add";
|
||||
"Add new tag:" = "Add new tag:";
|
||||
"all" = "all";
|
||||
|
||||
@ -73,15 +73,12 @@ class BookmarkLabelsViewModel {
|
||||
|
||||
@MainActor
|
||||
func addLabel(to bookmarkId: String, label: String) async {
|
||||
let individualLabels = label
|
||||
.components(separatedBy: " ")
|
||||
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
||||
.filter { !$0.isEmpty }
|
||||
.filter { !currentLabels.contains($0) }
|
||||
let splitLabels = LabelUtils.splitLabelsFromInput(label)
|
||||
let uniqueLabels = LabelUtils.filterUniqueLabels(splitLabels, currentLabels: currentLabels)
|
||||
|
||||
guard !individualLabels.isEmpty else { return }
|
||||
guard !uniqueLabels.isEmpty else { return }
|
||||
|
||||
await addLabels(to: bookmarkId, labels: individualLabels)
|
||||
await addLabels(to: bookmarkId, labels: uniqueLabels)
|
||||
newLabelText = ""
|
||||
searchText = ""
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
// Created by Ilyas Hallak on 01.07.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum BookmarkState: String, CaseIterable {
|
||||
case all = "all"
|
||||
case unread = "unread"
|
||||
@ -14,13 +16,13 @@ enum BookmarkState: String, CaseIterable {
|
||||
var displayName: String {
|
||||
switch self {
|
||||
case .all:
|
||||
return "All"
|
||||
return NSLocalizedString("All", comment: "")
|
||||
case .unread:
|
||||
return "Unread"
|
||||
return NSLocalizedString("Unread", comment: "")
|
||||
case .favorite:
|
||||
return "Favorites"
|
||||
return NSLocalizedString("Favorites", comment: "")
|
||||
case .archived:
|
||||
return "Archive"
|
||||
return NSLocalizedString("Archive", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -103,12 +103,12 @@ struct PadSidebarView: View {
|
||||
case .tags:
|
||||
NavigationStack {
|
||||
LabelsView(selectedTag: $selectedTag)
|
||||
.navigationDestination(item: $selectedTag) { label in
|
||||
BookmarksView(state: .all, type: [], selectedBookmark: $selectedBookmark, tag: label.name)
|
||||
.navigationTitle("\(label.name) (\(label.count))")
|
||||
.onDisappear {
|
||||
selectedTag = nil
|
||||
}
|
||||
}
|
||||
.navigationDestination(item: $selectedTag) { label in
|
||||
BookmarksView(state: .all, type: [], selectedBookmark: $selectedBookmark, tag: label.name)
|
||||
.navigationTitle("\(label.name) (\(label.count))")
|
||||
.onDisappear {
|
||||
selectedTag = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@
|
||||
// Created by Ilyas Hallak on 01.07.25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
|
||||
enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
||||
case search, all, unread, favorite, archived, article, videos, pictures, tags, settings
|
||||
|
||||
@ -12,16 +14,16 @@ enum SidebarTab: Hashable, CaseIterable, Identifiable {
|
||||
|
||||
var label: String {
|
||||
switch self {
|
||||
case .all: return "All"
|
||||
case .unread: return "Unread"
|
||||
case .favorite: return "Favorites"
|
||||
case .archived: return "Archive"
|
||||
case .search: return "Search"
|
||||
case .settings: return "Settings"
|
||||
case .article: return "Articles"
|
||||
case .videos: return "Videos"
|
||||
case .pictures: return "Pictures"
|
||||
case .tags: return "Tags"
|
||||
case .all: return NSLocalizedString("All", comment: "")
|
||||
case .unread: return NSLocalizedString("Unread", comment: "")
|
||||
case .favorite: return NSLocalizedString("Favorites", comment: "")
|
||||
case .archived: return NSLocalizedString("Archive", comment: "")
|
||||
case .search: return NSLocalizedString("Search", comment: "")
|
||||
case .settings: return NSLocalizedString("Settings", comment: "")
|
||||
case .article: return NSLocalizedString("Articles", comment: "")
|
||||
case .videos: return NSLocalizedString("Videos", comment: "")
|
||||
case .pictures: return NSLocalizedString("Pictures", comment: "")
|
||||
case .tags: return NSLocalizedString("Tags", comment: "")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ struct AppearanceSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: "Appearance", icon: "paintbrush")
|
||||
SectionHeader(title: "Appearance".localized, icon: "paintbrush")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Theme Section
|
||||
|
||||
@ -9,7 +9,7 @@ struct CacheSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: "Cache Settings", icon: "internaldrive")
|
||||
SectionHeader(title: "Cache Settings".localized, icon: "internaldrive")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
VStack(spacing: 12) {
|
||||
|
||||
@ -16,7 +16,7 @@ struct FontSettingsView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: "Font Settings", icon: "textformat")
|
||||
SectionHeader(title: "Font Settings".localized, icon: "textformat")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
// Font Family Picker
|
||||
|
||||
96
readeck/UI/Settings/LegalNoticeView.swift
Normal file
96
readeck/UI/Settings/LegalNoticeView.swift
Normal file
@ -0,0 +1,96 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LegalNoticeView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Legal Notice")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
sectionView(
|
||||
title: "App Publisher",
|
||||
content: """
|
||||
Ilyas Hallak
|
||||
[Street Address]
|
||||
[City, Postal Code]
|
||||
[Country]
|
||||
|
||||
Email: ilhallak@gmail.com
|
||||
"""
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Content Responsibility",
|
||||
content: "The publisher is responsible for the content of this application in accordance with applicable laws."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "App Information",
|
||||
content: """
|
||||
readeck iOS - Bookmark Management Client
|
||||
Version: \(Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "Unknown")
|
||||
Build: \(Bundle.main.infoDictionary?["CFBundleVersion"] as? String ?? "Unknown")
|
||||
|
||||
This app is an open source client for readeck bookmark management.
|
||||
"""
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "License",
|
||||
content: "This software is released under the MIT License. The source code is available at the official repository."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Disclaimer",
|
||||
content: "The app is provided \"as is\" without warranty of any kind. The publisher assumes no liability for damages arising from the use of this application."
|
||||
)
|
||||
|
||||
// TODO: Add business registration details if needed
|
||||
// sectionView(
|
||||
// title: "Business Registration",
|
||||
// content: """
|
||||
// [Business Registration Number]
|
||||
// [Tax ID / VAT Number]
|
||||
// [Responsible Authority]
|
||||
// """
|
||||
// )
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sectionView(title: String, content: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text(content)
|
||||
.font(.body)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LegalNoticeView()
|
||||
}
|
||||
125
readeck/UI/Settings/LegalPrivacySettingsView.swift
Normal file
125
readeck/UI/Settings/LegalPrivacySettingsView.swift
Normal file
@ -0,0 +1,125 @@
|
||||
import SwiftUI
|
||||
|
||||
struct LegalPrivacySettingsView: View {
|
||||
@State private var showingPrivacyPolicy = false
|
||||
@State private var showingLegalNotice = false
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: "Legal & Privacy".localized, icon: "doc.text")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
VStack(spacing: 16) {
|
||||
// Privacy Policy
|
||||
Button(action: {
|
||||
showingPrivacyPolicy = true
|
||||
}) {
|
||||
HStack {
|
||||
Text(NSLocalizedString("Privacy Policy", comment: ""))
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Legal Notice
|
||||
Button(action: {
|
||||
showingLegalNotice = true
|
||||
}) {
|
||||
HStack {
|
||||
Text(NSLocalizedString("Legal Notice", comment: ""))
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
Divider()
|
||||
.padding(.vertical, 8)
|
||||
|
||||
// Support Section
|
||||
VStack(spacing: 12) {
|
||||
// Report an Issue
|
||||
Button(action: {
|
||||
if let url = URL(string: "https://github.com/ilyas-hallak/readeck-ios/issues") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text(NSLocalizedString("Report an Issue", comment: ""))
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
|
||||
// Contact Support
|
||||
Button(action: {
|
||||
if let url = URL(string: "mailto:hi@ilyashallak.de?subject=readeck%20iOS") {
|
||||
UIApplication.shared.open(url)
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Text(NSLocalizedString("Contact Support", comment: ""))
|
||||
.font(.headline)
|
||||
.foregroundColor(.primary)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "arrow.up.right")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
.padding(.horizontal, 16)
|
||||
.padding(.vertical, 12)
|
||||
.background(Color(.systemBackground))
|
||||
.clipShape(RoundedRectangle(cornerRadius: 10))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $showingPrivacyPolicy) {
|
||||
PrivacyPolicyView()
|
||||
}
|
||||
.sheet(isPresented: $showingLegalNotice) {
|
||||
LegalNoticeView()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
LegalPrivacySettingsView()
|
||||
.cardStyle()
|
||||
.padding()
|
||||
}
|
||||
82
readeck/UI/Settings/PrivacyPolicyView.swift
Normal file
82
readeck/UI/Settings/PrivacyPolicyView.swift
Normal file
@ -0,0 +1,82 @@
|
||||
import SwiftUI
|
||||
|
||||
struct PrivacyPolicyView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ScrollView {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text("Privacy Policy")
|
||||
.font(.largeTitle)
|
||||
.fontWeight(.bold)
|
||||
.padding(.bottom, 10)
|
||||
|
||||
Text("Last updated: [DATE]")
|
||||
.font(.caption)
|
||||
.foregroundColor(.secondary)
|
||||
|
||||
VStack(alignment: .leading, spacing: 16) {
|
||||
sectionView(
|
||||
title: "Data Collection",
|
||||
content: "readeck iOS does not collect, store, or transmit any personal data. The app operates as a client for your personal readeck server and all data remains on your device or your own server infrastructure."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Local Storage",
|
||||
content: "The app stores bookmarks locally on your device using CoreData for offline access. Login credentials are securely stored in the iOS Keychain. No data is shared with third parties."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Server Communication",
|
||||
content: "The app communicates only with your configured readeck server to synchronize bookmarks. No analytics, tracking, or telemetry data is collected or transmitted."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Third-Party Services",
|
||||
content: "This app does not use any third-party analytics, advertising, or tracking services. It does not integrate with social media platforms or other external services."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Your Rights",
|
||||
content: "Since no personal data is collected, processed, or stored by us, there is no personal data to access, modify, or delete from our side. All your data is under your control on your device and server."
|
||||
)
|
||||
|
||||
sectionView(
|
||||
title: "Contact",
|
||||
content: "If you have questions about this privacy policy, please contact us at: ilhallak@gmail.com"
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.navigationTitle("")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .navigationBarTrailing) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private func sectionView(title: String, content: String) -> some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(title)
|
||||
.font(.headline)
|
||||
.fontWeight(.semibold)
|
||||
|
||||
Text(content)
|
||||
.font(.body)
|
||||
.foregroundColor(.primary)
|
||||
}
|
||||
.padding(.vertical, 4)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
PrivacyPolicyView()
|
||||
}
|
||||
@ -33,6 +33,9 @@ struct SettingsContainerView: View {
|
||||
SettingsServerView()
|
||||
.cardStyle()
|
||||
|
||||
LegalPrivacySettingsView()
|
||||
.cardStyle()
|
||||
|
||||
// Debug-only Logging Configuration
|
||||
if Bundle.main.isDebugBuild {
|
||||
debugSettingsSection
|
||||
|
||||
@ -16,7 +16,7 @@ struct SettingsGeneralView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: "General Settings", icon: "gear")
|
||||
SectionHeader(title: "General Settings".localized, icon: "gear")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
VStack(alignment: .leading, spacing: 12) {
|
||||
|
||||
@ -18,7 +18,7 @@ struct SettingsServerView: View {
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 20) {
|
||||
SectionHeader(title: viewModel.isSetupMode ? "Server Settings" : "Server Connection", icon: "server.rack")
|
||||
SectionHeader(title: viewModel.isSetupMode ? "Server Settings".localized : "Server Connection".localized, icon: "server.rack")
|
||||
.padding(.bottom, 4)
|
||||
|
||||
Text(viewModel.isSetupMode ?
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user