diff --git a/Zapp/Assets.xcassets/ZappIcon.imageset/Contents.json b/Zapp/Assets.xcassets/ZappIcon.imageset/Contents.json new file mode 100644 index 00000000..7a3f81e7 --- /dev/null +++ b/Zapp/Assets.xcassets/ZappIcon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ZappIcon.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/Zapp/Assets.xcassets/ZappIcon.imageset/ZappIcon.png b/Zapp/Assets.xcassets/ZappIcon.imageset/ZappIcon.png new file mode 100644 index 00000000..39b31680 Binary files /dev/null and b/Zapp/Assets.xcassets/ZappIcon.imageset/ZappIcon.png differ diff --git a/Zapp/Persistence/PersistenceManager.swift b/Zapp/Persistence/PersistenceManager.swift index 88086f31..4b1b4432 100644 --- a/Zapp/Persistence/PersistenceManager.swift +++ b/Zapp/Persistence/PersistenceManager.swift @@ -8,6 +8,7 @@ extension Notification.Name { static let continueWatchingUpdated = Notification.Name("PersistenceManager.continueWatchingUpdated") static let bookmarksUpdated = Notification.Name("PersistenceManager.bookmarksUpdated") static let navigateToDownloadsTab = Notification.Name("MainTab.navigateToDownloads") + static let showOnboardingAgain = Notification.Name("AppSettings.showOnboardingAgain") } private let persistenceLogger = Logger(subsystem: Bundle.main.bundleIdentifier ?? "Zapp", category: "Persistence") diff --git a/Zapp/Resources/de.lproj/Localizable.strings b/Zapp/Resources/de.lproj/Localizable.strings index c0f13450..59cef44e 100644 --- a/Zapp/Resources/de.lproj/Localizable.strings +++ b/Zapp/Resources/de.lproj/Localizable.strings @@ -227,6 +227,43 @@ // Geo Restriction "geo_restriction_title" = "Eingeschränkte Nutzung außerhalb Deutschlands"; -"geo_restriction_message_generic" = "Die App ist hauptsächlich für die Nutzung innerhalb Deutschlands konzipiert. Im Ausland können Streams blockiert sein oder fehlschlagen."; +"geo_restriction_message_generic" = "Die App ist für die Nutzung innerhalb Deutschlands konzipiert. Im Ausland können Streams blockiert sein oder fehlschlagen."; "geo_restriction_message_region" = "Dein Gerät befindet sich in %@, daher funktionieren manche Livestreams und Mediathek-Inhalte eventuell nicht wie vorgesehen."; "geo_restriction_cta" = "Trotzdem fortfahren"; + +// Onboarding +"onboarding_welcome_title" = "Zapp"; +"onboarding_welcome_subtitle" = "Live-Sender und Mediatheken streamen"; +"onboarding_continue" = "Weiter"; +"onboarding_get_started" = "Los geht's"; + +"onboarding_features_title" = "Features"; +"onboarding_features_subtitle" = "Alles, was du brauchst"; + +"onboarding_feature_live_title" = "Live Streams"; +"onboarding_feature_live_description" = "Schaue deine Lieblingssender live"; + +"onboarding_feature_mediathek_title" = "Mediatheken"; +"onboarding_feature_mediathek_description" = "Zugriff auf tausende Sendungen"; + +"onboarding_feature_continue_title" = "Weiterschauen"; +"onboarding_feature_continue_description" = "Setze fort, wo du aufgehört hast"; + +"onboarding_feature_bookmarks_title" = "Lesezeichen"; +"onboarding_feature_bookmarks_description" = "Speichere deine Favoriten"; + +"onboarding_feature_opensource_title" = "Open Source"; +"onboarding_feature_opensource_description" = "Transparent und kostenlos"; + +"onboarding_geo_title" = "Verfügbarkeit in Deutschland"; +"onboarding_geo_message_1" = "Diese App ist für die Nutzung in Deutschland konzipiert."; +"onboarding_geo_message_2" = "Im Ausland können Streams blockiert sein."; + +"onboarding_disclaimer_title" = "Wichtiger Hinweis"; +"onboarding_disclaimer_message_1" = "Diese App steht in keiner Verbindung zu den Sendern oder Mediatheken."; +"onboarding_disclaimer_message_2" = "Es werden ausschließlich die offiziellen Streams der öffentlich-rechtlichen Sender angezeigt."; + +"onboarding_finish_title" = "Viel Spaß!"; +"onboarding_finish_subtitle" = "Entdecke jetzt Live-TV und Mediatheken"; + +"settings_show_onboarding" = "Onboarding erneut anzeigen"; diff --git a/Zapp/Resources/en.lproj/Localizable.strings b/Zapp/Resources/en.lproj/Localizable.strings index fd3145af..392ee98a 100644 --- a/Zapp/Resources/en.lproj/Localizable.strings +++ b/Zapp/Resources/en.lproj/Localizable.strings @@ -227,6 +227,43 @@ // Geo Restriction "geo_restriction_title" = "Limited availability outside Germany"; -"geo_restriction_message_generic" = "This app is primarily build for use within Germany. Streams may fail or stay blocked when you're abroad."; +"geo_restriction_message_generic" = "This app is designed for use within Germany. When used abroad, streams may be blocked or fail."; "geo_restriction_message_region" = "Your device is located in %@, so some live streams and Mediathek videos may not work as expected."; "geo_restriction_cta" = "Continue anyway"; + +// Onboarding +"onboarding_welcome_title" = "Zapp"; +"onboarding_welcome_subtitle" = "Stream live TV and media libraries"; +"onboarding_continue" = "Continue"; +"onboarding_get_started" = "Get Started"; + +"onboarding_features_title" = "Features"; +"onboarding_features_subtitle" = "Everything you need"; + +"onboarding_feature_live_title" = "Live Streams"; +"onboarding_feature_live_description" = "Watch your favorite channels live"; + +"onboarding_feature_mediathek_title" = "Media Libraries"; +"onboarding_feature_mediathek_description" = "Access thousands of shows"; + +"onboarding_feature_continue_title" = "Continue Watching"; +"onboarding_feature_continue_description" = "Pick up where you left off"; + +"onboarding_feature_bookmarks_title" = "Bookmarks"; +"onboarding_feature_bookmarks_description" = "Save your favorites"; + +"onboarding_feature_opensource_title" = "Open Source"; +"onboarding_feature_opensource_description" = "Transparent and free"; + +"onboarding_geo_title" = "Availability in Germany"; +"onboarding_geo_message_1" = "This app is designed for use in Germany."; +"onboarding_geo_message_2" = "Outside Germany, streams may be blocked."; + +"onboarding_disclaimer_title" = "Important Notice"; +"onboarding_disclaimer_message_1" = "This app is not affiliated with any broadcasters or media libraries."; +"onboarding_disclaimer_message_2" = "Only official streams from public broadcasters are displayed."; + +"onboarding_finish_title" = "Enjoy!"; +"onboarding_finish_subtitle" = "Discover Live TV and media libraries"; + +"settings_show_onboarding" = "Show onboarding again"; diff --git a/Zapp/Settings/AppSettings.swift b/Zapp/Settings/AppSettings.swift index 66423c23..77203af9 100644 --- a/Zapp/Settings/AppSettings.swift +++ b/Zapp/Settings/AppSettings.swift @@ -16,6 +16,7 @@ final class AppSettings: ObservableObject { private let detailLandscapeKey = "detailLandscape" private let colorSchemePreferenceKey = "colorSchemePreference" private let streamHostKey = "streamHost" + private let hasCompletedOnboardingKey = "hasCompletedOnboarding" @Published var streamQualityWifi: MediathekShow.Quality { didSet { defaults.set(streamQualityWifi.rawValue, forKey: streamQualityWifiKey) } @@ -44,6 +45,9 @@ final class AppSettings: ObservableObject { didSet { defaults.set(streamHost, forKey: streamHostKey) } } + @Published var hasCompletedOnboarding: Bool { + didSet { defaults.set(hasCompletedOnboarding, forKey: hasCompletedOnboardingKey) } + } private init() { self.initialSystemInterfaceStyle = UIScreen.main.traitCollection.userInterfaceStyle @@ -53,6 +57,7 @@ final class AppSettings: ObservableObject { self.detailLandscape = defaults.bool(forKey: detailLandscapeKey) self.colorSchemePreference = ColorSchemePreference(rawValue: defaults.string(forKey: colorSchemePreferenceKey) ?? ColorSchemePreference.light.rawValue) ?? .light self.streamHost = defaults.string(forKey: streamHostKey) ?? "" + self.hasCompletedOnboarding = defaults.bool(forKey: hasCompletedOnboardingKey) applyColorSchemePreference() } diff --git a/Zapp/Views/OnboardingView.swift b/Zapp/Views/OnboardingView.swift new file mode 100644 index 00000000..b186eb3a --- /dev/null +++ b/Zapp/Views/OnboardingView.swift @@ -0,0 +1,290 @@ +import SwiftUI + +struct OnboardingView: View { + @Binding var isPresented: Bool + @State private var currentPage = 0 + + private let pages: [OnboardingPage] = [ + .welcome, + .features, + .geoRestriction, + .disclaimer, + .finish + ] + + var body: some View { + ZStack { + Color(.systemBackground) + .ignoresSafeArea() + + VStack(spacing: 0) { + TabView(selection: $currentPage) { + ForEach(0..