diff --git a/.swiftlint.yml b/.swiftlint.yml index b6dcc96e7..8fa216d80 100644 --- a/.swiftlint.yml +++ b/.swiftlint.yml @@ -19,6 +19,7 @@ opt_in_rules: # some rules are only opt-in excluded: # paths to ignore during linting. Takes precedence over `included`. - Carthage - DerivedData + - build - Pods - DerivedData - Core/CoreTests diff --git a/Authorization/Authorization.xcodeproj/project.pbxproj b/Authorization/Authorization.xcodeproj/project.pbxproj index 08d312520..d28b41e7b 100644 --- a/Authorization/Authorization.xcodeproj/project.pbxproj +++ b/Authorization/Authorization.xcodeproj/project.pbxproj @@ -45,6 +45,7 @@ DE843D6BB1B9DDA398494890 /* Pods_App_Authorization.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 47BCFB7C19382EECF15131B6 /* Pods_App_Authorization.framework */; }; E03261642AE64676002CA7EB /* StartupViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261632AE64676002CA7EB /* StartupViewModel.swift */; }; E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261652AE64AF4002CA7EB /* StartupView.swift */; }; + E03261682AE64AF4002CA7EB /* SandboxSplashView.swift in Sources */ = {isa = PBXBuildFile; fileRef = E03261672AE64AF4002CA7EB /* SandboxSplashView.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -119,6 +120,7 @@ CEB25A002CC13A36007FC792 /* SocialAuthResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SocialAuthResponse.swift; sourceTree = ""; }; E03261632AE64676002CA7EB /* StartupViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupViewModel.swift; sourceTree = ""; }; E03261652AE64AF4002CA7EB /* StartupView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StartupView.swift; sourceTree = ""; }; + E03261672AE64AF4002CA7EB /* SandboxSplashView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxSplashView.swift; sourceTree = ""; }; E78971D8E6ED2116BBF9FD66 /* Pods-App-Authorization.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.release.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.release.xcconfig"; sourceTree = ""; }; F52826C68AEA1CF4769389EA /* Pods-App-Authorization.releasestage.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releasestage.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releasestage.xcconfig"; sourceTree = ""; }; F5802BBA113276950ABCD9B3 /* Pods-App-Authorization.releaseprod.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App-Authorization.releaseprod.xcconfig"; path = "Target Support Files/Pods-App-Authorization/Pods-App-Authorization.releaseprod.xcconfig"; sourceTree = ""; }; @@ -364,6 +366,7 @@ children = ( E03261632AE64676002CA7EB /* StartupViewModel.swift */, E03261652AE64AF4002CA7EB /* StartupView.swift */, + E03261672AE64AF4002CA7EB /* SandboxSplashView.swift */, ); path = Startup; sourceTree = ""; @@ -580,6 +583,7 @@ 02F3BFE5292533720051930C /* AuthorizationRouter.swift in Sources */, 99C1654F2C0C4F5900DC384D /* SSOWebView.swift in Sources */, E03261662AE64AF4002CA7EB /* StartupView.swift in Sources */, + E03261682AE64AF4002CA7EB /* SandboxSplashView.swift in Sources */, 071009C728D1DA4F00344290 /* SignInViewModel.swift in Sources */, BA8B3A322AD5487300D25EF5 /* SocialAuthView.swift in Sources */, 99C1654D2C0C4F2F00DC384D /* SSOHelper.swift in Sources */, diff --git a/Authorization/Authorization/Presentation/Login/SignInView.swift b/Authorization/Authorization/Presentation/Login/SignInView.swift index f92309117..10827f116 100644 --- a/Authorization/Authorization/Presentation/Login/SignInView.swift +++ b/Authorization/Authorization/Presentation/Login/SignInView.swift @@ -53,7 +53,7 @@ public struct SignInView: View { ThemeAssets.appLogo.swiftUIImage .resizable() .aspectRatio(contentMode: .fit) - .frame(maxWidth: 189, maxHeight: 89) + .frame(maxWidth: 263, maxHeight: 76) .padding(.top, isHorizontal ? 20 : 40) .padding(.bottom, isHorizontal ? 10 : 40) .accessibilityIdentifier("logo_image") diff --git a/Authorization/Authorization/Presentation/Startup/SandboxSplashView.swift b/Authorization/Authorization/Presentation/Startup/SandboxSplashView.swift new file mode 100644 index 000000000..2012d2b3a --- /dev/null +++ b/Authorization/Authorization/Presentation/Startup/SandboxSplashView.swift @@ -0,0 +1,100 @@ +// +// SandboxSplashView.swift +// Authorization +// + +import SwiftUI +import Core +import Theme + +public struct SandboxSplashView: View { + + @Environment(\.isHorizontal) private var isHorizontal + + private let router: AuthorizationRouter + + public init(router: AuthorizationRouter) { + self.router = router + } + + public var body: some View { + ZStack(alignment: .top) { + VStack { + ThemeAssets.headerBackground.swiftUIImage + .resizable() + .edgesIgnoringSafeArea(.top) + .accessibilityIdentifier("sandbox_splash_bg_image") + } + .frame(maxWidth: .infinity, maxHeight: 200) + + VStack(alignment: .center) { + ThemeAssets.appLogo.swiftUIImage + .resizable() + .aspectRatio(contentMode: .fit) + .frame(maxWidth: 263, maxHeight: 76) + .padding(.top, isHorizontal ? 20 : 40) + .padding(.bottom, isHorizontal ? 10 : 40) + .accessibilityIdentifier("sandbox_splash_logo") + + GeometryReader { proxy in + ScrollView { + VStack(alignment: .center, spacing: 16) { + Text("Welcome to the Open edX\u{00AE} Mobile App") + .font(Theme.Fonts.displaySmall) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .padding(.top, 8) + .accessibilityIdentifier("sandbox_splash_title") + + Text( + "This app was built for you to explore the Open edX\u{00AE} Mobile App. " + + "Courses are for demonstration purposes only." + ) + .font(Theme.Fonts.titleSmall) + .foregroundColor(Theme.Colors.textPrimary) + .multilineTextAlignment(.center) + .accessibilityIdentifier("sandbox_splash_subtitle") + + StyledButton( + "Start Exploring", + action: { + router.showLoginScreen(sourceScreen: .default) + }, + iconImage: CoreAssets.arrowRight16.swiftUIImage, + iconPosition: .right + ) + .frame(maxWidth: .infinity) + .padding(.top, 24) + .accessibilityIdentifier("sandbox_splash_start_button") + + Spacer() + } + .padding(.horizontal, 24) + .padding(.top, 50) + .frameLimit(width: proxy.size.width) + } + .roundedBackground(Theme.Colors.loginBackground) + } + } + } + .navigationBarHidden(true) + .ignoresSafeArea(.all, edges: .horizontal) + .background(Theme.Colors.background.ignoresSafeArea(.all)) + } +} + +#if DEBUG +struct SandboxSplashView_Previews: PreviewProvider { + static var previews: some View { + SandboxSplashView(router: AuthorizationRouterMock()) + .preferredColorScheme(.light) + .previewDisplayName("SandboxSplashView Light") + .loadFonts() + + SandboxSplashView(router: AuthorizationRouterMock()) + .preferredColorScheme(.dark) + .previewDisplayName("SandboxSplashView Dark") + .loadFonts() + } +} +#endif diff --git a/Core/Core/Configuration/Connectivity.swift b/Core/Core/Configuration/Connectivity.swift index 0b5c57fdb..c40eae68b 100644 --- a/Core/Core/Configuration/Connectivity.swift +++ b/Core/Core/Configuration/Connectivity.swift @@ -28,14 +28,17 @@ public class Connectivity: ConnectivityProtocol { private let verificationURL: URL private let verificationTimeout: TimeInterval private let cacheValidity: TimeInterval = 30 + private let offlineDebounce: TimeInterval = 1.5 private var lastVerificationDate: TimeInterval? private var lastVerificationResult: Bool = true + private var offlineTask: Task? public let internetReachableSubject = CurrentValueSubject(nil) private(set) var _isInternetAvailable: Bool = true { didSet { + guard oldValue != _isInternetAvailable else { return } Task { @MainActor in internetReachableSubject.send(_isInternetAvailable ? .reachable : .notReachable) } @@ -71,9 +74,10 @@ public class Connectivity: ConnectivityProtocol { Task { @MainActor in switch status { case .reachable: + self.cancelOfflineDebounce() await self.performVerification() case .notReachable, .unknown: - self.updateAvailability(false, at: 0) + self.scheduleOffline() } } } @@ -83,6 +87,20 @@ public class Connectivity: ConnectivityProtocol { networkManager?.stopListening() } + private func scheduleOffline() { + offlineTask?.cancel() + offlineTask = Task { @MainActor in + try? await Task.sleep(nanoseconds: UInt64(offlineDebounce * 1_000_000_000)) + guard !Task.isCancelled else { return } + updateAvailability(false, at: 0) + } + } + + private func cancelOfflineDebounce() { + offlineTask?.cancel() + offlineTask = nil + } + private func performVerification() async { let now = Date().timeIntervalSince1970 let live = await verifyInternet() diff --git a/Core/Core/View/Base/OfflineSnackBarView.swift b/Core/Core/View/Base/OfflineSnackBarView.swift index ad6c70130..7876a5cba 100644 --- a/Core/Core/View/Base/OfflineSnackBarView.swift +++ b/Core/Core/View/Base/OfflineSnackBarView.swift @@ -69,7 +69,9 @@ public struct OfflineSnackBarView: View { dismiss = false } case .reachable: - break + withAnimation { + dismiss = true + } case .none: break } @@ -77,9 +79,11 @@ public struct OfflineSnackBarView: View { } } +#if DEBUG struct OfflineSnackBarView_Previews: PreviewProvider { static var previews: some View { let configMock = ConfigMock() OfflineSnackBarView(connectivity: Connectivity(config: configMock), reloadAction: {}) } } +#endif diff --git a/Core/Core/View/Base/WebBrowser.swift b/Core/Core/View/Base/WebBrowser.swift index 6ae177cb4..50e13bce3 100644 --- a/Core/Core/View/Base/WebBrowser.swift +++ b/Core/Core/View/Base/WebBrowser.swift @@ -79,9 +79,11 @@ public struct WebBrowser: View { } } +#if DEBUG struct WebBrowser_Previews: PreviewProvider { static var previews: some View { let configMock = ConfigMock() WebBrowser(url: "", pageTitle: "", connectivity: Connectivity(config: configMock)) } } +#endif diff --git a/Course/Course/Presentation/Unit/Subviews/WebView.swift b/Course/Course/Presentation/Unit/Subviews/WebView.swift index 935884a28..51825d511 100644 --- a/Course/Course/Presentation/Unit/Subviews/WebView.swift +++ b/Course/Course/Presentation/Unit/Subviews/WebView.swift @@ -23,7 +23,7 @@ struct WebView: View { url: url, dataUrl: localUrl, viewModel: Container.shared.resolve(WebUnitViewModel.self)!, - connectivity: Connectivity(config: ConfigMock()), + connectivity: Connectivity(config: Container.shared.resolve(ConfigProtocol.self)!), injections: injections, blockID: blockID ) diff --git a/Open edX Sandbox.ssgen/project.json b/Open edX Sandbox.ssgen/project.json new file mode 100644 index 000000000..a2fa3b77e --- /dev/null +++ b/Open edX Sandbox.ssgen/project.json @@ -0,0 +1,155 @@ +{ + "appInfo" : { + "description" : "Open edx mobile application in a test environment.", + "name" : "Open edX Sandbox" + }, + "backgroundGroups" : [ + { + "createdAt" : "2026-03-16T13:51:48Z", + "id" : "9E42C8F6-B4A0-4BD3-9A55-EBED3B1F5D4D", + "panoramicImageFilename" : "panoramic_9E42C8F6-B4A0-4BD3-9A55-EBED3B1F5D4D.png", + "prompt" : "Generate a BACKGROUND image for App Store marketing.\n\nUSE EXACTLY THESE COLORS (mandatory):\n- #3E5EC2\n- #082B4D\n- #CC3165\n\nThe background MUST use these exact colors or darker\/more saturated versions of them.\nCreate smooth gradients and large soft shapes using ONLY these colors.\n\nDESIGN REQUIREMENTS:\n- Minimalist flat design\n- Large soft rounded geometric shapes\n- Smooth color transitions between the 3 colors\n- Premium, professional, advertising-ready aesthetic\n\nSTRICTLY FORBIDDEN:\n- ANY white, off-white, cream, or light colors\n- ANY shadows or drop shadows\n- ANY lighting effects or glows\n- Colors NOT in the palette above\n- Text, icons, UI elements, people, objects, devices\n\nComposition: Asymmetric balanced layout with 2-3 large rounded forms on dark background from palette\n\nWhite text must be easily readable on the background.", + "style" : "abstract" + }, + { + "createdAt" : "2026-03-16T13:51:42Z", + "id" : "6FB20265-02CB-4061-ADC3-CE55D3FEECA1", + "panoramicImageFilename" : "panoramic_6FB20265-02CB-4061-ADC3-CE55D3FEECA1.png", + "prompt" : "Generate a BACKGROUND image for App Store marketing.\n\nUSE EXACTLY THESE COLORS (mandatory):\n- #3E5EC2\n- #082B4D\n- #CC3165\n\nThe background MUST use these exact colors or darker\/more saturated versions of them.\nCreate smooth gradients and large soft shapes using ONLY these colors.\n\nDESIGN REQUIREMENTS:\n- Minimalist flat design\n- Large soft rounded geometric shapes\n- Smooth color transitions between the 3 colors\n- Premium, professional, advertising-ready aesthetic\n\nSTRICTLY FORBIDDEN:\n- ANY white, off-white, cream, or light colors\n- ANY shadows or drop shadows\n- ANY lighting effects or glows\n- Colors NOT in the palette above\n- Text, icons, UI elements, people, objects, devices\n\nComposition: Centered radial composition with soft expanding circles\n\nWhite text must be easily readable on the background.", + "style" : "abstract" + }, + { + "createdAt" : "2026-03-16T13:51:34Z", + "id" : "FC40496F-BDC1-470F-A9C1-17484E613DA4", + "panoramicImageFilename" : "panoramic_FC40496F-BDC1-470F-A9C1-17484E613DA4.png", + "prompt" : "Generate a BACKGROUND image for App Store marketing.\n\nUSE EXACTLY THESE COLORS (mandatory):\n- #3E5EC2\n- #082B4D\n- #CC3165\n\nThe background MUST use these exact colors or darker\/more saturated versions of them.\nCreate smooth gradients and large soft shapes using ONLY these colors.\n\nDESIGN REQUIREMENTS:\n- Minimalist flat design\n- Large soft rounded geometric shapes\n- Smooth color transitions between the 3 colors\n- Premium, professional, advertising-ready aesthetic\n\nSTRICTLY FORBIDDEN:\n- ANY white, off-white, cream, or light colors\n- ANY shadows or drop shadows\n- ANY lighting effects or glows\n- Colors NOT in the palette above\n- Text, icons, UI elements, people, objects, devices\n\nComposition: Large diagonal soft shapes flowing from bottom-left to top-right\n\nWhite text must be easily readable on the background.", + "style" : "abstract" + } + ], + "colorPalette" : { + "accentColor" : "#CC3266", + "allColors" : [ + "#19212F", + "#E6E7E8", + "#092B4E", + "#97A5BB", + "#C2C3BD", + "#18191A", + "#3250A8", + "#C3C8C4" + ], + "backgroundColor" : "#E6E7E8", + "primaryColor" : "#19212F", + "secondaryColor" : "#092B4E", + "suggestedGradients" : [ + [ + "#19212F", + "#41567B" + ], + [ + "#19212F", + "#092B4E" + ], + [ + "#41567B", + "#000000" + ], + [ + "#CC3266", + "#FF3E7F" + ] + ] + }, + "customGradients" : [ + + ], + "designVariants" : [ + { + "createdAt" : "2026-03-16T13:43:34Z", + "globalBackgroundColors" : [ + + ], + "globalBackgroundStyle" : "ai_generated:panoramic", + "globalDeviceFrame" : "iphone-deep-blue", + "globalIPadDeviceFrame" : "ipad-stroke-black", + "id" : "EF313D21-6805-4B4D-AA0C-6BD2BE742208", + "name" : "Variant A", + "screenshots" : [ + { + "backgroundColors" : [ + + ], + "backgroundStyle" : "ai_generated:panoramic", + "deviceFrame" : "iphone-deep-blue", + "id" : "6B4BA7EF-1CBE-4D17-B35D-8D36622DFD26", + "iphoneImageFilename" : "6B4BA7EF-1CBE-4D17-B35D-8D36622DFD26_iphone.png", + "layout" : "device_top", + "overlayElements" : [ + + ], + "subtitle" : "Track progress and continue your learning journey.", + "title" : "Your Personalized Learning Hub" + }, + { + "backgroundColors" : [ + + ], + "backgroundStyle" : "ai_generated:panoramic", + "deviceFrame" : "iphone-deep-blue", + "id" : "38C0D365-07CE-4A4F-941D-F0D0EA7F0ED6", + "iphoneImageFilename" : "38C0D365-07CE-4A4F-941D-F0D0EA7F0ED6_iphone.png", + "layout" : "device_bottom", + "overlayElements" : [ + + ], + "subtitle" : "Understand content, objectives, and enroll instantly.", + "title" : "Dive Into Course Details" + }, + { + "backgroundColors" : [ + + ], + "backgroundStyle" : "ai_generated:panoramic", + "deviceFrame" : "iphone-deep-blue", + "id" : "F9FCE3B6-BE0C-4828-AC79-60BD86B1AE4D", + "iphoneImageFilename" : "F9FCE3B6-BE0C-4828-AC79-60BD86B1AE4D_iphone.png", + "layout" : "device_top", + "overlayElements" : [ + + ], + "subtitle" : "Search and explore courses effortlessly.", + "title" : "Discover New Courses" + }, + { + "backgroundColors" : [ + + ], + "backgroundStyle" : "ai_generated:panoramic", + "deviceFrame" : "iphone-deep-blue", + "id" : "53B18FC8-BEC1-4A9B-B28B-9515C840502C", + "iphoneImageFilename" : "53B18FC8-BEC1-4A9B-B28B-9515C840502C_iphone.png", + "layout" : "device_bottom", + "overlayElements" : [ + + ], + "subtitle" : "Navigate lessons, track progress, and learn efficiently.", + "title" : "Master Your Coursework" + } + ], + "selectedBackgroundGroupId" : "9E42C8F6-B4A0-4BD3-9A55-EBED3B1F5D4D" + } + ], + "deviceType" : "iPhone", + "isPrimaryColorAutoDetected" : true, + "localization" : { + "selectedLanguageIds" : [ + + ], + "sourceLanguageId" : "en-US" + }, + "primaryColorHex" : "#18202E", + "selectedGroupId" : "9E42C8F6-B4A0-4BD3-9A55-EBED3B1F5D4D", + "selectedScreenshotIndex" : 0, + "selectedVariantIndex" : 0, + "version" : "1.0" +} \ No newline at end of file diff --git a/OpenEdX.xcodeproj/project.pbxproj b/OpenEdX.xcodeproj/project.pbxproj index ee7d5fbaa..448471f13 100644 --- a/OpenEdX.xcodeproj/project.pbxproj +++ b/OpenEdX.xcodeproj/project.pbxproj @@ -14,6 +14,7 @@ 0219C67828F4347600D64452 /* Course.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 0219C67628F4347600D64452 /* Course.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 022213D22C0E08E500B917E6 /* ProfilePersistence.swift in Sources */ = {isa = PBXBuildFile; fileRef = 022213D12C0E08E500B917E6 /* ProfilePersistence.swift */; }; 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */; }; + AA0000012D07000000000001 /* SandboxDisclaimerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = AA0000012D07000000000002 /* SandboxDisclaimerView.swift */; }; 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */; }; 025DE1A428DB4DAE0053E0F4 /* Profile.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; }; 025DE1A528DB4DAE0053E0F4 /* Profile.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 025DE1A328DB4DAE0053E0F4 /* Profile.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; @@ -105,6 +106,7 @@ 022213D12C0E08E500B917E6 /* ProfilePersistence.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfilePersistence.swift; sourceTree = ""; }; 02450ABD29C35FF20094E2D0 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainScreenViewModel.swift; sourceTree = ""; }; + AA0000012D07000000000002 /* SandboxDisclaimerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SandboxDisclaimerView.swift; sourceTree = ""; }; 025AD4AB2A6FB95C00AB8FA7 /* DatabaseManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DatabaseManager.swift; sourceTree = ""; }; 025C77A028E463E900B3DFA3 /* CourseOutline.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = CourseOutline.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 025DE1A328DB4DAE0053E0F4 /* Profile.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; path = Profile.framework; sourceTree = BUILT_PRODUCTS_DIR; }; @@ -212,6 +214,7 @@ children = ( 0727878D28D347C7002E9142 /* MainScreenView.swift */, 024E691F2AEFC3FB00FA0B59 /* MainScreenViewModel.swift */, + AA0000012D07000000000002 /* SandboxDisclaimerView.swift */, ); path = View; sourceTree = ""; @@ -588,6 +591,7 @@ A500668D2B6143000024680B /* FCMProvider.swift in Sources */, 025AD4AC2A6FB95C00AB8FA7 /* DatabaseManager.swift in Sources */, 024E69202AEFC3FB00FA0B59 /* MainScreenViewModel.swift in Sources */, + AA0000012D07000000000001 /* SandboxDisclaimerView.swift in Sources */, 065275372BB1B4070093BCCA /* PipManager.swift in Sources */, 0770DE2028D0858A006D8A5D /* Router.swift in Sources */, CEE5EDEE2D6E0A290089F67C /* DownloadsPersistence.swift in Sources */, @@ -715,7 +719,7 @@ FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_CFBundleDisplayName = "Sandbox Stage"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; @@ -729,7 +733,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.stage; + PRODUCT_BUNDLE_IDENTIFIER = org.sandbox.app.stage; PRODUCT_NAME = "$(TARGET_NAME) Stage"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -807,7 +811,7 @@ FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_CFBundleDisplayName = "Sandbox Stage"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; @@ -821,7 +825,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.stage; + PRODUCT_BUNDLE_IDENTIFIER = org.sandbox.app.stage; PRODUCT_NAME = "$(TARGET_NAME) Stage"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -905,7 +909,7 @@ FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_CFBundleDisplayName = "Sandbox Dev"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; @@ -919,7 +923,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.dev; + PRODUCT_BUNDLE_IDENTIFIER = org.sandbox.app.dev; PRODUCT_NAME = "$(TARGET_NAME) Dev"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -997,7 +1001,7 @@ FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_CFBundleDisplayName = "Sandbox Dev"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; @@ -1011,7 +1015,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app.dev; + PRODUCT_BUNDLE_IDENTIFIER = org.sandbox.app.dev; PRODUCT_NAME = "$(TARGET_NAME) Dev"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1149,7 +1153,7 @@ FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_CFBundleDisplayName = "Sandbox"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; @@ -1163,7 +1167,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app; + PRODUCT_BUNDLE_IDENTIFIER = org.sandbox.app; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; @@ -1187,7 +1191,7 @@ FULLSTORY_ENABLED = NO; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = OpenEdX/Info.plist; - INFOPLIST_KEY_CFBundleDisplayName = ""; + INFOPLIST_KEY_CFBundleDisplayName = "Sandbox"; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.education"; INFOPLIST_KEY_NSCalendarsUsageDescription = "We would like to utilize your calendar list to subscribe you to your personalized calendar for this course."; INFOPLIST_KEY_NSPhotoLibraryAddUsageDescription = "Allow access to your photo library so you can save photos in your gallery."; @@ -1201,7 +1205,7 @@ "@executable_path/Frameworks", ); MARKETING_VERSION = 1.0.0; - PRODUCT_BUNDLE_IDENTIFIER = org.openedx.app; + PRODUCT_BUNDLE_IDENTIFIER = org.sandbox.app; PRODUCT_NAME = "$(TARGET_NAME)"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; diff --git a/OpenEdX/Assets.xcassets/AppIcon.appiconset/Contents.json b/OpenEdX/Assets.xcassets/AppIcon.appiconset/Contents.json index 1a9ee3949..c97400b2c 100644 --- a/OpenEdX/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/OpenEdX/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,7 +1,7 @@ { "images" : [ { - "filename" : "app icon.jpg", + "filename" : "applogo.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/OpenEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg b/OpenEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg deleted file mode 100644 index debe3c4c2..000000000 Binary files a/OpenEdX/Assets.xcassets/AppIcon.appiconset/app icon.jpg and /dev/null differ diff --git a/OpenEdX/Assets.xcassets/AppIcon.appiconset/applogo.png b/OpenEdX/Assets.xcassets/AppIcon.appiconset/applogo.png new file mode 100644 index 000000000..70976aefc Binary files /dev/null and b/OpenEdX/Assets.xcassets/AppIcon.appiconset/applogo.png differ diff --git a/OpenEdX/Assets.xcassets/SplashBackground.colorset/Contents.json b/OpenEdX/Assets.xcassets/SplashBackground.colorset/Contents.json index 99fc4a9bb..1ffc23315 100644 --- a/OpenEdX/Assets.xcassets/SplashBackground.colorset/Contents.json +++ b/OpenEdX/Assets.xcassets/SplashBackground.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xFE", - "green" : "0x7B", - "red" : "0x51" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0x2F", - "green" : "0x21", - "red" : "0x19" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/OpenEdX/Assets.xcassets/appLogo.imageset/Contents.json b/OpenEdX/Assets.xcassets/appLogo.imageset/Contents.json index c0e621b99..a5410eb29 100644 --- a/OpenEdX/Assets.xcassets/appLogo.imageset/Contents.json +++ b/OpenEdX/Assets.xcassets/appLogo.imageset/Contents.json @@ -1,20 +1,20 @@ { "images" : [ - { - "filename" : "appLogo.svg", - "idiom" : "universal" - }, - { - "appearances" : [ - { - "appearance" : "luminosity", - "value" : "dark" - } - ], - "filename" : "appLogo-light.svg", - "idiom" : "universal" - } - ], + { + "filename" : "applogosplash_light.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "applogosplash_light 1.png", + "idiom" : "universal" + } + ], "info" : { "author" : "xcode", "version" : 1 diff --git a/OpenEdX/Assets.xcassets/appLogo.imageset/appLogo-light.svg b/OpenEdX/Assets.xcassets/appLogo.imageset/appLogo-light.svg deleted file mode 100644 index 0bc57dd59..000000000 --- a/OpenEdX/Assets.xcassets/appLogo.imageset/appLogo-light.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenEdX/Assets.xcassets/appLogo.imageset/appLogo.svg b/OpenEdX/Assets.xcassets/appLogo.imageset/appLogo.svg deleted file mode 100644 index 0bc57dd59..000000000 --- a/OpenEdX/Assets.xcassets/appLogo.imageset/appLogo.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/OpenEdX/Assets.xcassets/appLogo.imageset/applogosplash_light 1.png b/OpenEdX/Assets.xcassets/appLogo.imageset/applogosplash_light 1.png new file mode 100644 index 000000000..483e948b2 Binary files /dev/null and b/OpenEdX/Assets.xcassets/appLogo.imageset/applogosplash_light 1.png differ diff --git a/OpenEdX/Assets.xcassets/appLogo.imageset/applogosplash_light.png b/OpenEdX/Assets.xcassets/appLogo.imageset/applogosplash_light.png new file mode 100644 index 000000000..483e948b2 Binary files /dev/null and b/OpenEdX/Assets.xcassets/appLogo.imageset/applogosplash_light.png differ diff --git a/OpenEdX/RouteController.swift b/OpenEdX/RouteController.swift index f9a377ffe..eac214d91 100644 --- a/OpenEdX/RouteController.swift +++ b/OpenEdX/RouteController.swift @@ -11,28 +11,31 @@ import Core import Authorization import WhatsNew import Swinject +import Theme class RouteController: UIViewController { - + private lazy var navigation: UINavigationController = { diContainer.resolve(UINavigationController.self)! }() - + private lazy var appStorage: CoreStorage = { diContainer.resolve(CoreStorage.self)! }() - + private lazy var analytics: AuthorizationAnalytics = { diContainer.resolve(AuthorizationAnalytics.self)! }() - + private lazy var coreAnalytics: CoreAnalytics = { diContainer.resolve(CoreAnalytics.self)! }() - + + private var disclaimerController: UIViewController? + override func viewDidLoad() { super.viewDidLoad() - + if let user = appStorage.user, appStorage.accessToken != nil { analytics.identify(id: "\(user.id)", username: user.username ?? "", email: user.email ?? "") DispatchQueue.main.async { @@ -43,9 +46,44 @@ class RouteController: UIViewController { self.showStartupScreen() } } - + resetAppSupportDirectoryUserData() coreAnalytics.trackEvent(.launch, biValue: .launch) + +// DispatchQueue.main.async { +// self.showSandboxDisclaimer() +// } + } + + private func showSandboxDisclaimer() { + let disclaimerView = SandboxDisclaimerView { [weak self] in + self?.dismissSandboxDisclaimer() + } + let controller = UIHostingController(rootView: disclaimerView) + controller.modalPresentationStyle = .overFullScreen + controller.modalTransitionStyle = .crossDissolve + self.disclaimerController = controller + + // Present on top of whatever is currently showing + DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { + if let topController = self.topPresentedController() { + topController.present(controller, animated: true) + } + } + } + + private func dismissSandboxDisclaimer() { + disclaimerController?.dismiss(animated: true) { [weak self] in + self?.disclaimerController = nil + } + } + + private func topPresentedController() -> UIViewController? { + var top: UIViewController? = self + while let presented = top?.presentedViewController, presented !== disclaimerController { + top = presented + } + return top } private func showStartupScreen() { @@ -56,11 +94,8 @@ class RouteController: UIViewController { present(navigation, animated: false) } else { let controller = UIHostingController( - rootView: SignInView( - viewModel: diContainer.resolve( - SignInViewModel.self, - argument: LogistrationSourceScreen.default - )! + rootView: SandboxSplashView( + router: diContainer.resolve(AuthorizationRouter.self)! ) ) navigation.viewControllers = [controller] diff --git a/OpenEdX/Router.swift b/OpenEdX/Router.swift index d334f34e0..55827289d 100644 --- a/OpenEdX/Router.swift +++ b/OpenEdX/Router.swift @@ -137,11 +137,8 @@ public class Router: AuthorizationRouter, let controller = UIHostingController(rootView: view) navigationController.setViewControllers([controller], animated: true) } else { - let view = SignInView( - viewModel: Container.shared.resolve( - SignInViewModel.self, - argument: LogistrationSourceScreen.default - )! + let view = SandboxSplashView( + router: Container.shared.resolve(AuthorizationRouter.self)! ) let controller = UIHostingController(rootView: view) navigationController.setViewControllers([controller], animated: false) diff --git a/OpenEdX/View/SandboxDisclaimerView.swift b/OpenEdX/View/SandboxDisclaimerView.swift new file mode 100644 index 000000000..1d71ba7f6 --- /dev/null +++ b/OpenEdX/View/SandboxDisclaimerView.swift @@ -0,0 +1,115 @@ +// +// SandboxDisclaimerView.swift +// OpenEdX +// +// Created on 16.03.2026. +// + +import SwiftUI +import Theme + +struct SandboxDisclaimerView: View { + + @State private var appearAnimation = false + @State private var progress: CGFloat = 0 + + let onDismiss: () -> Void + + private let duration: TimeInterval = 5 + + var body: some View { + ZStack { + Theme.Colors.accentColor + .ignoresSafeArea() + + VStack(spacing: 24) { + Spacer() + + // Icon + Image(systemName: "flask.fill") + .font(.system(size: 56)) + .foregroundColor(.white.opacity(0.9)) + .scaleEffect(appearAnimation ? 1 : 0.5) + .opacity(appearAnimation ? 1 : 0) + .animation(.spring(response: 0.6, dampingFraction: 0.7), value: appearAnimation) + + // Title + Text("Sandbox Environment") + .font(Theme.Fonts.displaySmall) + .foregroundColor(.white) + .multilineTextAlignment(.center) + .opacity(appearAnimation ? 1 : 0) + .offset(y: appearAnimation ? 0 : 20) + .animation(.easeOut(duration: 0.5).delay(0.2), value: appearAnimation) + + // Description + VStack(spacing: 12) { + Text("You are using a test instance of Open edX") + .font(Theme.Fonts.titleMedium) + .foregroundColor(.white.opacity(0.95)) + .multilineTextAlignment(.center) + + Text( + "This environment is for testing and demonstration " + + "purposes only. Courses, progress, and account data " + + "may be reset at any time without notice." + ) + .font(Theme.Fonts.bodyLarge) + .foregroundColor(.white.opacity(0.8)) + .multilineTextAlignment(.center) + .lineSpacing(4) + + Text("sandbox.openedx.org") + .font(Theme.Fonts.labelLarge) + .foregroundColor(.white.opacity(0.6)) + .padding(.top, 4) + } + .padding(.horizontal, 32) + .opacity(appearAnimation ? 1 : 0) + .offset(y: appearAnimation ? 0 : 20) + .animation(.easeOut(duration: 0.5).delay(0.35), value: appearAnimation) + + Spacer() + + // Progress bar + VStack(spacing: 8) { + GeometryReader { geo in + Capsule() + .fill(Color.white.opacity(0.2)) + .frame(height: 4) + .overlay(alignment: .leading) { + Capsule() + .fill(Color.white.opacity(0.7)) + .frame(width: geo.size.width * progress, height: 4) + } + } + .frame(height: 4) + .padding(.horizontal, 48) + } + .opacity(appearAnimation ? 1 : 0) + .animation(.easeOut(duration: 0.3).delay(0.5), value: appearAnimation) + + // Skip button + Button { + onDismiss() + } label: { + Text("Tap to continue") + .font(Theme.Fonts.labelLarge) + .foregroundColor(.white.opacity(0.5)) + } + .padding(.bottom, 40) + .opacity(appearAnimation ? 1 : 0) + .animation(.easeOut(duration: 0.3).delay(0.6), value: appearAnimation) + } + } + .onAppear { + appearAnimation = true + withAnimation(.linear(duration: duration)) { + progress = 1 + } + DispatchQueue.main.asyncAfter(deadline: .now() + duration) { + onDismiss() + } + } + } +} diff --git a/Sandbox (3)/apply_android_theme.py b/Sandbox (3)/apply_android_theme.py new file mode 100644 index 000000000..48eea5308 --- /dev/null +++ b/Sandbox (3)/apply_android_theme.py @@ -0,0 +1,764 @@ +#!/usr/bin/env python3 +"""Apply exported Open edX mobile theme to the Android project. + +Usage: + # Copy this script into the exported theme folder (with theme.json + images/) + # then run it after placing the folder inside the Android repo + python3 apply_android_theme.py [--dry-run] [--theme PATH] [--project PATH] + +Defaults: + --theme : directory containing theme.json (defaults to the script location) + --project : Android repo root (auto-detected by walking upwards from theme dir) +""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, Iterable, Optional, Tuple + +THEME_JSON_NAME = "theme.json" +COLOR_FILE = Path("core/src/openedx/org/openedx/core/ui/theme/Colors.kt") +LIGHT_COLORS_XML = Path("core/src/main/res/values/colors.xml") +DARK_COLORS_XML = Path("core/src/main/res/values-night/colors.xml") +LIGHT_HEADER = Path("core/src/main/res/drawable/core_top_header.png") +DARK_HEADER = Path("core/src/main/res/drawable-night/core_top_header.png") +LOGO_LIGHT = Path("core/src/main/res/drawable/core_ic_logo.png") +LOGO_DARK = Path("core/src/main/res/drawable-night/core_ic_logo.png") +LOGO_XML_LIGHT = Path("core/src/main/res/drawable/core_ic_logo.xml") +LOGO_XML_DARK = Path("core/src/main/res/drawable-night/core_ic_logo.xml") +DIMENS_FILE = Path("core/src/main/res/values/core_theme_generated_dimens.xml") +DIMEN_WIDTH_NAME = "core_login_logo_width" +DIMEN_HEIGHT_NAME = "core_login_logo_height" +DEFAULT_LOGO_WIDTH_DP = 171.0 +DEFAULT_LOGO_HEIGHT_DP = 48.0 + +ANDROID_RES_DIR = Path("app/src/main/res") +ANDROID_DRAWABLE_DIR = ANDROID_RES_DIR / "drawable" +ANDROID_MIPMAP_GLOB = "mipmap-*" +LAUNCHER_BACKGROUND_XML = ANDROID_DRAWABLE_DIR / "ic_launcher_background.xml" +LAUNCHER_FOREGROUND_DRAWABLE = ANDROID_DRAWABLE_DIR / "app_icon_foreground.png" +LAUNCHER_ADAPTIVE_XML = ANDROID_RES_DIR / "mipmap-anydpi-v26/ic_launcher.xml" +LAUNCHER_ADAPTIVE_ROUND_XML = ANDROID_RES_DIR / "mipmap-anydpi-v26/ic_launcher_round.xml" +SPLASH_ICON_DRAWABLE = ANDROID_DRAWABLE_DIR / "app_splash_icon.png" + +FOREGROUND_BASE_SCALE = 0.72 +MAX_ANDROID_ICON_SCALE = 1.0 / FOREGROUND_BASE_SCALE + +CONFIG_BASE_DIR = Path("default_config") +CONFIG_ENVIRONMENTS = { + "prod": {"suffix": "", "bundle_suffix": ""}, + "stage": {"suffix": " Stage", "bundle_suffix": ".stage"}, + "dev": {"suffix": " Dev", "bundle_suffix": ".dev"}, +} + +DEFAULT_ACCENT = { + "light": "#3C68FF", + "dark": "#5378F8", +} + +# Maps theme.json color key → (light Colors.kt names, dark Colors.kt names) +COLOR_KT_MAPPINGS = ( + ("accentColor", ( + "light_primary", "light_info_variant", "light_text_accent", + "light_text_hyper_link", "light_primary_button_background", + "light_primary_button_bordered_text", + ), ( + "dark_primary", "dark_info_variant", "dark_text_hyper_link", + "dark_primary_button_background", "dark_primary_button_bordered_text", + )), + ("background", ("light_background",), ("dark_background",)), + ("textPrimary", ("light_text_primary",), ("dark_text_primary",)), + ("textSecondary", ("light_text_secondary",), ("dark_text_secondary",)), + ("textInputBackground", ("light_text_field_background",), ("dark_text_field_background",)), + ("textInputStroke", ("light_text_field_border",), ("dark_text_field_border",)), + ("primaryButtonText", ("light_primary_button_text",), ("dark_primary_button_text",)), +) + +LIGHT_XML_COLOR_NAMES = ("primary", "checked_tab_item") +DARK_XML_COLOR_NAMES = ("primary", "checked_tab_item") + + +@dataclass +class CopyTask: + description: str + json_key: str + dest_path: Path + allow_fallback: bool = True + + +HEADER_COPY_TASKS = ( + CopyTask("Login header (light)", "headerBackground", LIGHT_HEADER, True), + CopyTask("Login header (dark)", "headerBackground", DARK_HEADER, True), +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Apply exported Open edX theme to Android project") + parser.add_argument("--project", type=Path, default=None, + help="Path to Android repo (defaults to parent directories of theme folder)") + parser.add_argument("--theme", type=Path, default=None, + help="Theme export directory (defaults to folder containing this script)") + parser.add_argument("--dry-run", action="store_true", help="Preview changes without writing") + return parser.parse_args() + + +def resolve_theme_dir(theme_arg: Optional[Path]) -> Path: + if theme_arg: + candidate = theme_arg.resolve() + else: + candidate = Path(__file__).resolve().parent + if not (candidate / THEME_JSON_NAME).exists(): + raise FileNotFoundError( + f"theme.json not found in {candidate}. Use --theme to point to the export folder." + ) + return candidate + + +def iter_candidate_roots(start: Path, limit: int = 6) -> Iterable[Path]: + current = start + for _ in range(limit): + yield current + if current.parent == current: + break + current = current.parent + + +def resolve_project_root(theme_dir: Path, project_arg: Optional[Path]) -> Path: + candidates = [] + if project_arg: + candidates.append(project_arg) + candidates.extend(iter_candidate_roots(theme_dir)) + seen = set() + for candidate in candidates: + resolved = candidate.resolve() + if resolved in seen: + continue + seen.add(resolved) + if (resolved / COLOR_FILE).exists() and (resolved / "settings.gradle").exists(): + return resolved + raise FileNotFoundError("Could not locate Android project root; use --project to specify it explicitly.") + + +def load_theme(theme_dir: Path) -> Dict: + with (theme_dir / THEME_JSON_NAME).open("r", encoding="utf-8") as fh: + return json.load(fh) + + +def get_accent(theme: Dict, mode: str) -> str: + colors = theme.get(mode, {}).get("colors", {}) + value = colors.get("accentColor") + if value: + return normalize_hex(value) + fallback = DEFAULT_ACCENT[mode] + print(f"[WARN] accentColor missing for {mode}; using default {fallback}") + return normalize_hex(fallback) + + +def normalize_hex(value: str) -> str: + v = value.strip().lstrip('#') + if len(v) == 3: + v = ''.join(ch * 2 for ch in v) + elif len(v) not in (6, 8): + raise ValueError(f"Unsupported hex color: {value}") + if len(v) == 8: + v = v[0:6] # drop alpha from #AARRGGBB => use RGB only, alpha handled separately + return '#' + v.upper() + + +def to_argb(hex_value: str) -> str: + v = normalize_hex(hex_value)[1:] + return '0xFF' + v.upper() + + +def update_colors_kt(path: Path, theme: Dict, dry_run: bool) -> None: + text = path.read_text(encoding="utf-8") + light_colors = theme.get("light", {}).get("colors", {}) + dark_colors = theme.get("dark", {}).get("colors", {}) + + replacements: Dict[str, str] = {} + for json_key, light_names, dark_names in COLOR_KT_MAPPINGS: + light_val = light_colors.get(json_key) + dark_val = dark_colors.get(json_key) + if light_val: + argb = to_argb(light_val) + for name in light_names: + replacements[name] = argb + if dark_val: + argb = to_argb(dark_val) + for name in dark_names: + replacements[name] = argb + + updated_text = text + status = "[DRY-RUN]" if dry_run else "[OK]" + for name, value in replacements.items(): + # Match Color(0x...) literal + pat_literal = re.compile(rf"(val\s+{name}\s*=\s*Color\()(0x[0-9A-Fa-f]{{6,8}})(\))") + match = pat_literal.search(updated_text) + if match: + updated_text = pat_literal.sub(lambda m: f"{m.group(1)}{value}{m.group(3)}", updated_text, count=1) + print(f"{status} Colors.kt: set {name}={value}") + else: + # Match variable reference or Color.White/Color.Black etc. + pat_ref = re.compile(rf"(val\s+{name}\s*=\s*)\S+") + match_ref = pat_ref.search(updated_text) + if match_ref: + updated_text = pat_ref.sub(rf"\g<1>Color({value})", updated_text, count=1) + print(f"{status} Colors.kt: set {name}=Color({value})") + else: + print(f"[WARN] Colors.kt: '{name}' definition not found") + + if dry_run: + return + path.write_text(updated_text, encoding="utf-8") + + +def _normalize_colors_xml(text: str) -> str: + """Ensure every tag starts on its own line with proper indentation.""" + text = re.sub(r"\s*\n \s*", "\n", text) + return text + + +def update_colors_xml(path: Path, names: Iterable[str], accent_hex: str, dry_run: bool) -> None: + text = path.read_text(encoding="utf-8") + updated_text = text + status = "[DRY-RUN]" if dry_run else "[OK]" + for name in names: + pattern = re.compile(rf"(]*>)(#[0-9A-Fa-f]+)()") + match = pattern.search(updated_text) + if match: + updated_text = pattern.sub(lambda m: f"{m.group(1)}{accent_hex}{m.group(3)}", updated_text, count=1) + print(f"{status} {path}: set {name}={accent_hex}") + else: + print(f"[WARN] {path}: color '{name}' not found") + updated_text = _normalize_colors_xml(updated_text) + if dry_run: + return + path.write_text(updated_text, encoding="utf-8") + + +def update_color_entry(path: Path, name: str, value: str, dry_run: bool) -> None: + if not path.exists(): + print(f"[WARN] Color file not found: {path}") + return + text = path.read_text(encoding="utf-8") + color = normalize_hex(value) + entry_pattern = re.compile( + rf"\s*]*>[^<]*\s*\n?", + re.IGNORECASE, + ) + updated = entry_pattern.sub('', text) + insert_fragment = f" {color}\n" + if '' in updated: + updated = updated.replace('', insert_fragment + '') + else: + updated = updated + insert_fragment + updated = _normalize_colors_xml(updated) + if updated == text: + return + if dry_run: + print(f"[DRY-RUN] Would update color '{name}' in {path}") + else: + path.write_text(updated, encoding="utf-8") + print(f"[OK] Updated color '{name}' in {path}") + + +ALLOWED_IMAGE_SUFFIXES = {'.png', '.webp'} + + +def copy_image(theme_dir: Path, rel_path: Optional[str], dest: Path, description: str, dry_run: bool) -> None: + if not rel_path: + print(f"[WARN] {description}: theme.json does not reference a file") + return + source = (theme_dir / rel_path).resolve() + if not source.exists(): + print(f"[WARN] {description}: source file missing ({source})") + return + suffix = source.suffix.lower() + if suffix not in ALLOWED_IMAGE_SUFFIXES: + print(f"[WARN] {description}: unsupported format {source.suffix}; skipping copy") + return + if dry_run: + print(f"[DRY-RUN] {description}: would copy {source} -> {dest}") + return + dest.parent.mkdir(parents=True, exist_ok=True) + shutil.copy2(source, dest) + print(f"[OK] {description}: copied to {dest}") + + +def remove_conflicting_resource(target: Path, dry_run: bool) -> None: + directory = target.parent + stem = target.stem + for ext in ('.png', '.webp', '.xml'): + candidate = directory / f"{stem}{ext}" + if candidate == target or not candidate.exists(): + continue + if dry_run: + print(f"[DRY-RUN] Would remove conflicting resource {candidate}") + else: + candidate.unlink() + print(f"[OK] Removed conflicting resource {candidate}") + + +def apply_header_images(theme: Dict, theme_dir: Path, project_root: Path, dry_run: bool) -> None: + for task in HEADER_COPY_TASKS: + mode = 'dark' if 'dark' in task.description.lower() else 'light' + images = theme.get(mode, {}).get('images', {}) + rel = images.get(task.json_key) + if not rel and task.allow_fallback: + other_mode = 'light' if mode == 'dark' else 'dark' + rel = theme.get(other_mode, {}).get('images', {}).get(task.json_key) + copy_image(theme_dir, rel, project_root / task.dest_path, task.description, dry_run) + +def remove_legacy_logo_xml(project_root: Path, dry_run: bool) -> None: + for path in (project_root / LOGO_XML_LIGHT, project_root / LOGO_XML_DARK): + if path.exists(): + if dry_run: + print(f"[DRY-RUN] Would remove legacy logo vector {path}") + else: + path.unlink() + print(f"[OK] Removed legacy logo vector {path}") + + +def choose_logo_dimensions(theme: Dict) -> Optional[Tuple[float, float]]: + for mode in ('light', 'dark'): + meta = theme.get(mode, {}).get('imagesMeta', {}) + meta_entry = meta.get('appLogoLogin') + if isinstance(meta_entry, dict): + width = meta_entry.get('width') + height = meta_entry.get('height') + if isinstance(width, (int, float)) and isinstance(height, (int, float)) and width > 0 and height > 0: + return float(width), float(height) + return None + + +def format_dimen(value: float) -> str: + return (f"{value:.2f}".rstrip('0').rstrip('.') if value % 1 else f"{int(value)}") + "dp" + + +def write_logo_dimens(project_root: Path, dims: Tuple[float, float], dry_run: bool) -> None: + width_dp, height_dp = dims + content = ( + "\n" + "\n" + f" {format_dimen(width_dp)}\n" + f" {format_dimen(height_dp)}\n" + "\n" + ) + dest = project_root / DIMENS_FILE + if dry_run: + print(f"[DRY-RUN] Would write logo dimensions to {dest}: {width_dp}dp × {height_dp}dp") + return + dest.parent.mkdir(parents=True, exist_ok=True) + dest.write_text(content, encoding="utf-8") + print(f"[OK] Wrote logo dimensions to {dest}") + + +def apply_logo_images(theme: Dict, theme_dir: Path, project_root: Path, dry_run: bool) -> None: + remove_legacy_logo_xml(project_root, dry_run) + + tasks = ( + ("Login logo (light)", 'light', LOGO_LIGHT), + ("Login logo (dark)", 'dark', LOGO_DARK), + ) + + for description, mode, target_rel_path in tasks: + images = theme.get(mode, {}).get('images', {}) + rel = images.get('appLogoLogin') + if not rel: + other_mode = 'light' if mode == 'dark' else 'dark' + rel = theme.get(other_mode, {}).get('images', {}).get('appLogoLogin') + copy_image(theme_dir, rel, project_root / target_rel_path, description, dry_run) + + dims = choose_logo_dimensions(theme) + if dims: + write_logo_dimens(project_root, dims, dry_run) + else: + print("[WARN] Logo dimensions not provided; using defaults") + write_logo_dimens(project_root, (DEFAULT_LOGO_WIDTH_DP, DEFAULT_LOGO_HEIGHT_DP), dry_run) + update_logo_composables(project_root, dry_run) + + +def ensure_kotlin_import(text: str, import_stmt: str) -> Tuple[str, bool]: + if import_stmt in text: + return text, False + lines = text.splitlines() + insert_idx = -1 + for idx, line in enumerate(lines): + if line.startswith("import "): + insert_idx = idx + if insert_idx == -1: + for idx, line in enumerate(lines): + if line.startswith("package "): + insert_idx = idx + break + if insert_idx == -1: + return text, False + lines.insert(insert_idx + 1, import_stmt) + if text.endswith("\n"): + updated = "\n".join(lines) + "\n" + else: + updated = "\n".join(lines) + return updated, True + + +def update_signin_logo(project_root: Path, dry_run: bool) -> bool: + path = project_root / "core/src/openedx/org/openedx/core/ui/theme/compose/SignInLogoView.kt" + if not path.exists(): + print(f"[WARN] SignInLogoView not found at {path}") + return False + text = path.read_text(encoding="utf-8") + if f"R.dimen.{DIMEN_WIDTH_NAME}" in text and f"R.dimen.{DIMEN_HEIGHT_NAME}" in text: + return False + changed = False + text, imp_changed = ensure_kotlin_import(text, "import androidx.compose.foundation.layout.size") + changed |= imp_changed + text, imp_changed = ensure_kotlin_import(text, "import androidx.compose.ui.res.dimensionResource") + changed |= imp_changed + if f"R.dimen.{DIMEN_WIDTH_NAME}" not in text: + simple = "modifier = Modifier.padding(top = 20.dp)" + block = "modifier = Modifier\n .padding(top = 20.dp)" + replacement = ( + "modifier = Modifier\n" + " .padding(top = 20.dp)\n" + " .size(\n" + f" width = dimensionResource(id = R.dimen.{DIMEN_WIDTH_NAME}),\n" + f" height = dimensionResource(id = R.dimen.{DIMEN_HEIGHT_NAME})\n" + " )" + ) + if simple in text: + text = text.replace(simple, replacement, 1) + changed = True + elif block in text: + text = text.replace(block, replacement, 1) + changed = True + else: + print(f"[WARN] Could not inject size modifier in {path}") + if changed: + if dry_run: + print(f"[DRY-RUN] Would update logo sizing in {path}") + else: + path.write_text(text, encoding="utf-8") + print(f"[OK] Updated logo sizing in {path}") + return changed + + +def update_logistration_logo(project_root: Path, dry_run: bool) -> bool: + path = project_root / "core/src/openedx/org/openedx/core/ui/theme/compose/LogistrationLogoView.kt" + if not path.exists(): + print(f"[WARN] LogistrationLogoView not found at {path}") + return False + text = path.read_text(encoding="utf-8") + if f"R.dimen.{DIMEN_WIDTH_NAME}" in text and f"R.dimen.{DIMEN_HEIGHT_NAME}" in text: + return False + changed = False + text, imp_changed = ensure_kotlin_import(text, "import androidx.compose.foundation.layout.size") + changed |= imp_changed + text, imp_changed = ensure_kotlin_import(text, "import androidx.compose.ui.res.dimensionResource") + changed |= imp_changed + if f"R.dimen.{DIMEN_WIDTH_NAME}" not in text: + block = ( + "modifier = Modifier\n" + " .padding(top = 64.dp, bottom = 20.dp)\n" + " .wrapContentWidth()" + ) + if block in text: + replacement = ( + "modifier = Modifier\n" + " .padding(top = 64.dp, bottom = 20.dp)\n" + " .size(\n" + f" width = dimensionResource(id = R.dimen.{DIMEN_WIDTH_NAME}),\n" + f" height = dimensionResource(id = R.dimen.{DIMEN_HEIGHT_NAME})\n" + " )\n" + " .wrapContentWidth()" + ) + text = text.replace(block, replacement, 1) + changed = True + else: + print(f"[WARN] Could not inject size modifier in {path}") + if changed: + if dry_run: + print(f"[DRY-RUN] Would update logo sizing in {path}") + else: + path.write_text(text, encoding="utf-8") + print(f"[OK] Updated logo sizing in {path}") + return changed + + +def update_logo_composables(project_root: Path, dry_run: bool) -> None: + update_signin_logo(project_root, dry_run) + update_logistration_logo(project_root, dry_run) + + +def apply_app_icon_new_spec(theme_dir: Path, project_root: Path, spec: Dict, accent_hex: str, dry_run: bool) -> None: + assets = spec.get("assets") + if not isinstance(assets, dict) or not assets: + print("[WARN] appIconAndroid.assets missing or empty; skipping Android icon update") + return + res_root = project_root / ANDROID_RES_DIR + if not res_root.exists(): + print(f"[WARN] Android res directory not found at {res_root}") + return + + for required in ( + "drawable/app_icon_foreground.png", + "mipmap-xxxhdpi/ic_launcher.png", + "mipmap-xxxhdpi/ic_launcher_round.png", + ): + if required not in assets: + print(f"[WARN] Expected Android icon asset '{required}' not provided; proceeding with available files") + + for rel_dest, rel_source in assets.items(): + if not isinstance(rel_dest, str) or not isinstance(rel_source, str): + print(f"[WARN] Invalid asset mapping {rel_dest!r} -> {rel_source!r}; expected strings") + continue + dest_path = res_root / rel_dest + description = f"Launcher asset {rel_dest}" + remove_conflicting_resource(dest_path, dry_run) + copy_image(theme_dir, rel_source, dest_path, description, dry_run) + + raw_background = spec.get("backgroundColor") + background = raw_background if isinstance(raw_background, str) and raw_background.strip() else None + scale_value = None + scale_raw = spec.get("scale") + if isinstance(scale_raw, (int, float)): + scale_value = clamp_scale(float(scale_raw), 0.1, 1.0 / FOREGROUND_BASE_SCALE) + use_background = bool(spec.get("useBackground", True)) + splash_color_light = spec.get("splashColorLight") or spec.get("splashColor") + splash_color_dark = spec.get("splashColorDark") or spec.get("splashColor") + + update_adaptive_icon_xml(project_root, dry_run) + if (project_root / LAUNCHER_BACKGROUND_XML).exists() and not dry_run: + (project_root / LAUNCHER_BACKGROUND_XML).unlink() + elif (project_root / LAUNCHER_BACKGROUND_XML).exists(): + print(f"[DRY-RUN] Would remove legacy launcher background drawable at {project_root / LAUNCHER_BACKGROUND_XML}") + + play_store_rel = spec.get("playStore") + if isinstance(play_store_rel, str): + play_store_dest = project_root / "app/src/main/ic_launcher-playstore.png" + remove_conflicting_resource(play_store_dest, dry_run) + copy_image(theme_dir, play_store_rel, play_store_dest, "Play Store icon", dry_run) + + if isinstance(splash_color_light, str) and splash_color_light.strip(): + update_color_entry(project_root / LIGHT_COLORS_XML, 'splash', splash_color_light, dry_run) + if isinstance(splash_color_dark, str) and splash_color_dark.strip(): + update_color_entry(project_root / DARK_COLORS_XML, 'splash', splash_color_dark, dry_run) + + +def apply_app_icon(theme: Dict, theme_dir: Path, project_root: Path, accent_hex: str, dry_run: bool) -> None: + icons = theme.get("icons", {}) + android_spec = icons.get("appIconAndroid") + if isinstance(android_spec, dict) and android_spec.get("assets"): + apply_app_icon_new_spec(theme_dir, project_root, android_spec, accent_hex, dry_run) + return + + rel = icons.get("appIcon") + if not rel: + print("[WARN] App icon not found in theme export (icons.appIcon)") + return + res_root = project_root / ANDROID_RES_DIR + if not res_root.exists(): + print(f"[WARN] Android res directory not found at {res_root}") + return + + targets: list[Tuple[Path, str]] = [] + for dirpath in res_root.glob(ANDROID_MIPMAP_GLOB): + if dirpath.name.endswith('v26'): + continue + for filename in ("ic_launcher.png", "ic_launcher_round.png", "ic_launcher_foreground.png"): + target = dirpath / filename + if target.exists(): + desc = f"Launcher icon {dirpath.name}/{filename}" + targets.append((target, desc)) + + if not targets: + print("[WARN] No launcher icon assets found to update") + for target, desc in targets: + copy_image(theme_dir, rel, target, desc, dry_run) + + # drawable copy for adaptive foreground + copy_image(theme_dir, rel, project_root / LAUNCHER_FOREGROUND_DRAWABLE, "Launcher foreground drawable", dry_run) + + update_launcher_background_xml(project_root, accent_hex, dry_run) + + +def clamp_scale(value: float, minimum: float = 0.1, maximum: float = MAX_ANDROID_ICON_SCALE) -> float: + return max(minimum, min(maximum, value)) + + +def update_adaptive_icon_xml(project_root: Path, dry_run: bool) -> None: + contents = ( + "\n" + "\n" + " \n" + " \n" + "\n" + ) + for rel in (LAUNCHER_ADAPTIVE_XML, LAUNCHER_ADAPTIVE_ROUND_XML): + path = project_root / rel + if not path.exists(): + continue + current = path.read_text(encoding="utf-8") + if current == contents: + continue + if dry_run: + print(f"[DRY-RUN] Would update adaptive icon XML at {path}") + else: + path.write_text(contents, encoding="utf-8") + print(f"[OK] Updated adaptive icon XML at {path}") + + +def update_launcher_background_xml(project_root: Path, accent_hex: str, dry_run: bool) -> None: + path = project_root / LAUNCHER_BACKGROUND_XML + color = normalize_hex(accent_hex) + content = ( + "\n \n\n" + ) + if not path.exists(): + if dry_run: + print(f"[DRY-RUN] Would create {path}") + else: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(content, encoding="utf-8") + print(f"[OK] Updated launcher background at {path}") + return + current = path.read_text(encoding="utf-8") + if current == content: + return + if dry_run: + print(f"[DRY-RUN] Would update launcher background at {path}") + else: + path.write_text(content, encoding="utf-8") + print(f"[OK] Updated launcher background at {path}") + + +def sanitize_bundle_component(theme_name: str) -> str: + lowered = theme_name.lower() + lowered = re.sub(r"\s+", ".", lowered) + lowered = re.sub(r"[^a-z0-9\.]+", "", lowered) + lowered = re.sub(r"\.+", ".", lowered).strip('.') + return lowered or "app" + + +def strip_env_suffix(app_id: str) -> str: + for suffix in (".stage", ".dev"): + if app_id.endswith(suffix): + return app_id[: -len(suffix)] + return app_id + + +def replace_app_id_component(app_id: str, slug: str) -> str: + tokens = [token for token in app_id.split('.') if token] + if not tokens: + return slug + target_index = None + for idx, token in enumerate(tokens): + if token.lower() == "openedx": + target_index = idx + break + if target_index is None: + target_index = 1 if len(tokens) > 1 else 0 + tokens[target_index] = slug + return '.'.join(tokens) + + +def extract_yaml_value(text: str, key: str) -> Optional[str]: + pattern = re.compile(rf"^{key}:\s*(['\"]?)([^'\"\n]+)\1\s*$", re.MULTILINE) + match = pattern.search(text) + if match: + return match.group(2).strip() + return None + + +def format_yaml_string(value: str) -> str: + escaped = value.replace('"', '\"') + return f'"{escaped}"' + + +def update_yaml_file(path: Path, replacements: Dict[str, str], dry_run: bool) -> None: + if not path.exists(): + print(f"[WARN] Config file not found: {path}") + return + original = path.read_text(encoding="utf-8") + updated = original + for key, value in replacements.items(): + pattern = re.compile(rf"^{key}:\s*.*$", re.MULTILINE) + line = f"{key}: {format_yaml_string(value)}" + if pattern.search(updated): + updated = pattern.sub(line, updated, count=1) + else: + updated = f"{line}\n" + updated + if updated == original: + return + if dry_run: + print(f"[DRY-RUN] Would update config {path}") + else: + path.write_text(updated, encoding="utf-8") + print(f"[OK] Updated config {path}") + + +def update_android_configs(theme_name: str, project_root: Path, dry_run: bool) -> None: + slug = sanitize_bundle_component(theme_name) + for env, opts in CONFIG_ENVIRONMENTS.items(): + config_path = project_root / CONFIG_BASE_DIR / env / "config.yaml" + if not config_path.exists(): + print(f"[WARN] Config for environment '{env}' not found at {config_path}") + continue + text = config_path.read_text(encoding="utf-8") + current_app_id = extract_yaml_value(text, "APPLICATION_ID") or "org.openedx.app" + base_app_id = strip_env_suffix(current_app_id) + replaced = replace_app_id_component(base_app_id, slug) + bundle_suffix = opts["bundle_suffix"] + new_app_id = replaced if not bundle_suffix else (replaced if replaced.endswith(bundle_suffix) else replaced + bundle_suffix) + display_name = f"{theme_name}{opts['suffix']}".strip() + replacements = { + "APPLICATION_ID": new_app_id, + "PLATFORM_NAME": display_name, + "PLATFORM_FULL_NAME": display_name, + "ENVIRONMENT_DISPLAY_NAME": display_name, + } + update_yaml_file(config_path, replacements, dry_run) + + +def main() -> int: + args = parse_args() + try: + theme_dir = resolve_theme_dir(args.theme) + project_root = resolve_project_root(theme_dir, args.project) + theme = load_theme(theme_dir) + except Exception as exc: + print(f"[ERROR] {exc}") + return 1 + + print(f"[INFO] Theme folder: {theme_dir}") + print(f"[INFO] Android project: {project_root}") + if args.dry_run: + print("[INFO] Dry run enabled – no files will be modified.") + + accent_light = get_accent(theme, 'light') + accent_dark = get_accent(theme, 'dark') + + update_colors_kt(project_root / COLOR_FILE, theme, args.dry_run) + update_colors_xml(project_root / LIGHT_COLORS_XML, LIGHT_XML_COLOR_NAMES, accent_light, args.dry_run) + update_colors_xml(project_root / DARK_COLORS_XML, DARK_XML_COLOR_NAMES, accent_dark, args.dry_run) + + apply_header_images(theme, theme_dir, project_root, args.dry_run) + apply_logo_images(theme, theme_dir, project_root, args.dry_run) + apply_app_icon(theme, theme_dir, project_root, accent_light, args.dry_run) + + theme_name = theme.get("name") or "Open edX" + update_android_configs(theme_name, project_root, args.dry_run) + + print("[DONE] Android theme application complete") + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/Sandbox (3)/apply_ios_theme.py b/Sandbox (3)/apply_ios_theme.py new file mode 100644 index 000000000..d56f423e3 --- /dev/null +++ b/Sandbox (3)/apply_ios_theme.py @@ -0,0 +1,524 @@ +#!/usr/bin/env python3 +"""Apply exported Open edX mobile theme to the iOS project. + +Copy this script into the exported branding folder (alongside theme.json +and images/) and run it after dropping that folder inside the iOS repo. + +Example: + cp apply_ios_theme.py path/to/exported_theme/ + cd path/to/openedx-app-ios/exported_theme + python3 apply_ios_theme.py +""" + +from __future__ import annotations + +import argparse +import json +import re +import shutil +import sys +from dataclasses import dataclass +from pathlib import Path +from typing import Dict, List, Optional, Sequence, Tuple + +THEME_JSON_NAME = "theme.json" +PROJECT_FILE_PATH = Path("OpenEdX.xcodeproj/project.pbxproj") + +# Environments present in the Xcode project and their display name suffixes. +ENV_DISPLAY_SUFFIX = { + "prod": "", + "stage": " Stage", + "dev": " Dev", +} + +@dataclass +class ColorTask: + description: str + relative_path: Path + light_key: Optional[str] + dark_key: Optional[str] + preserve_dark: bool = False + fallback_to_accent: bool = False + +@dataclass +class ImageTask: + description: str + relative_dir: Path + kind: str + include_dark: bool = True + ios_catalog: bool = True + + +COLOR_TASKS: Sequence[ColorTask] = ( + ColorTask("Accent color", Path("Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Accent button color", Path("Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Accent X color", Path("Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Background", Path("Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json"), + "background", "background"), + ColorTask("Login background", Path("Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json"), + "background", "background"), + ColorTask("Text primary", Path("Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json"), + "textPrimary", "textPrimary"), + ColorTask("Text secondary", Path("Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json"), + "textSecondary", "textSecondary"), + ColorTask("Info link color", Path("Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Text input background", Path("Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json"), + "textInputBackground", "textInputBackground"), + ColorTask("Text input stroke", Path("Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json"), + "textInputStroke", "textInputStroke"), + ColorTask("Primary button text", Path("Theme/Theme/Assets.xcassets/Colors/PrimaryButtonTextColor.colorset/Contents.json"), + "primaryButtonText", "primaryButtonText"), + ColorTask("Secondary button border", Path("Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Secondary button text", Path("Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Delete account background", Path("Theme/Theme/Assets.xcassets/Colors/DeleteAccountBG.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Resume button background", Path("Theme/Theme/Assets.xcassets/Colors/ResumeButton/ResumeButtonBG.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Social auth color", Path("Theme/Theme/Assets.xcassets/Colors/SocialAuthColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Toggle switch color", Path("Theme/Theme/Assets.xcassets/Colors/ToggleSwitchColor.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), + ColorTask("Sliding tab stroke", Path("Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json"), + "accentColor", None, preserve_dark=True, fallback_to_accent=True), + ColorTask("Sliding tab text", Path("Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json"), + "accentColor", None, preserve_dark=True, fallback_to_accent=True), + ColorTask("Splash background", Path("OpenEdX/Assets.xcassets/SplashBackground.colorset/Contents.json"), + "accentColor", "accentColor", fallback_to_accent=True), +) + +IMAGE_TASKS: Sequence[ImageTask] = ( + ImageTask("Login app logo", Path("Theme/Theme/Assets.xcassets/appLogo.imageset"), "appLogoLogin"), + ImageTask("Login header background", Path("Theme/Theme/Assets.xcassets/headerBackground.imageset"), "headerBackground"), + ImageTask("Splash app logo", Path("OpenEdX/Assets.xcassets/appLogo.imageset"), "appLogoSplash"), +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser(description="Apply exported Open edX mobile branding to the iOS app.") + parser.add_argument( + "--project", + type=Path, + default=None, + help="Path to openedx-app-ios repository (defaults to the parent folder of this script).", + ) + parser.add_argument( + "--theme", + type=Path, + default=None, + help="Path to exported theme folder containing theme.json and images/. Defaults to the folder where the script lives.", + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show planned changes without writing files.", + ) + return parser.parse_args() + + +def load_theme(theme_dir: Path) -> Dict: + theme_json_path = theme_dir / THEME_JSON_NAME + if not theme_json_path.exists(): + raise FileNotFoundError(f"theme.json not found at {theme_json_path}") + with theme_json_path.open("r", encoding="utf-8") as fh: + return json.load(fh) + + +def ensure_project(project_root: Path) -> None: + if not (project_root / PROJECT_FILE_PATH).exists(): + raise FileNotFoundError( + "Could not locate OpenEdX.xcodeproj/project.pbxproj under " + f"{project_root}. Use --project to point to the openedx-app-ios repo." + ) + + +def iter_candidate_roots(start: Path, limit: int = 6) -> Sequence[Path]: + current = start + yielded: List[Path] = [] + for _ in range(limit): + yielded.append(current) + if current.parent == current: + break + current = current.parent + return yielded + + +def resolve_project_root(theme_dir: Path, project_arg: Optional[Path]) -> Path: + candidates: List[Path] = [] + if project_arg is not None: + candidates.append(project_arg) + candidates.extend(iter_candidate_roots(theme_dir)) + + visited: List[Path] = [] + for candidate in candidates: + resolved = candidate.resolve() + if resolved in visited: + continue + visited.append(resolved) + if (resolved / PROJECT_FILE_PATH).exists(): + return resolved + + raise FileNotFoundError( + "Could not automatically locate OpenEdX.xcodeproj/project.pbxproj. " + "Use --project to specify the openedx-app-ios repository." + ) + + +def resolve_theme_dir(theme_arg: Optional[Path]) -> Path: + if theme_arg is not None: + candidate = theme_arg.resolve() + else: + candidate = Path(__file__).resolve().parent + if not (candidate / THEME_JSON_NAME).exists(): + raise FileNotFoundError( + f"theme.json not found in {candidate}. Use --theme to point to the exported theme folder." + ) + return candidate + + +def color_from_palette(palette: Dict[str, Dict], mode: str, key: Optional[str]) -> Optional[str]: + if key is None: + return None + colors = palette.get(mode, {}) + value = colors.get(key) + if value: + return value + # fallback to light when dark is missing + if mode == "dark": + value = palette.get("light", {}).get(key) + if value: + return value + return None + + +def hex_to_components(hex_color: str) -> Dict[str, str]: + value = hex_color.strip().lstrip("#") + if len(value) not in (6, 8): + raise ValueError(f"Unsupported color format: {hex_color}") + if len(value) == 6: + r, g, b = value[0:2], value[2:4], value[4:6] + a = "FF" + else: + r, g, b, a = value[0:2], value[2:4], value[4:6], value[6:8] + r_int = int(r, 16) / 255 + g_int = int(g, 16) / 255 + b_int = int(b, 16) / 255 + a_int = int(a, 16) / 255 + def fmt(component: float) -> str: + return f"{component:.3f}".rstrip("0").rstrip(".") if component not in (0, 1) else f"{component:.0f}.000" + return { + "red": fmt(r_int), + "green": fmt(g_int), + "blue": fmt(b_int), + "alpha": fmt(a_int), + } + + +def apply_color_asset(theme_palette: Dict[str, Dict], project_root: Path, task: ColorTask, dry_run: bool) -> None: + asset_path = project_root / task.relative_path + if not asset_path.exists(): + print(f"[WARN] Color asset missing: {asset_path}") + return + + with asset_path.open("r", encoding="utf-8") as fh: + data = json.load(fh) + + changes: List[Tuple[str, str]] = [] + for entry in data.get("colors", []): + is_dark = any( + appearance.get("appearance") == "luminosity" and appearance.get("value") == "dark" + for appearance in entry.get("appearances", []) + ) + mode = "dark" if is_dark else "light" + if is_dark and task.preserve_dark: + continue + target_hex = color_from_palette(theme_palette, mode, task.dark_key if is_dark else task.light_key) + if not target_hex and task.fallback_to_accent: + target_hex = color_from_palette(theme_palette, mode, "accentColor") + if not target_hex: + print(f"[WARN] {task.description}: no color for {mode} mode in theme.json") + continue + components = hex_to_components(target_hex) + entry.setdefault("color", {}).setdefault("components", {}).update(components) + changes.append((mode, target_hex)) + + if not changes: + print(f"[INFO] {task.description}: nothing to change") + return + + if not dry_run: + with asset_path.open("w", encoding="utf-8") as fh: + json.dump(data, fh, indent=2, ensure_ascii=False) + fh.write("\n") + status = "[OK]" + else: + status = "[DRY-RUN]" + + summary = ", ".join(f"{mode}={hex_value}" for mode, hex_value in changes) + print(f"{status} {task.description}: {summary} -> {asset_path}") + + +def clean_imageset(imageset_dir: Path, dry_run: bool) -> None: + for child in imageset_dir.iterdir(): + if child.name == "Contents.json": + continue + if child.is_file() and not dry_run: + child.unlink() + + +def prepare_contents_json(target: Path) -> Dict: + if target.exists(): + with target.open("r", encoding="utf-8") as fh: + try: + data = json.load(fh) + except json.JSONDecodeError: + data = {} + else: + data = {} + data.setdefault("info", {"author": "xcode", "version": 1}) + return data + + +def copy_image(theme_dir: Path, relative_path: Optional[str], images_dir: Path, dry_run: bool) -> Optional[str]: + if not relative_path: + return None + source = (theme_dir / relative_path).resolve() + if not source.exists(): + print(f"[WARN] Image {relative_path} not found under {theme_dir}") + return None + target_name = source.name + destination = images_dir / target_name + if not dry_run: + shutil.copy2(source, destination) + return target_name + + +def apply_image_asset(theme: Dict, theme_dir: Path, project_root: Path, task: ImageTask, dry_run: bool) -> None: + imageset_dir = project_root / task.relative_dir + contents_path = imageset_dir / "Contents.json" + if not imageset_dir.exists(): + print(f"[WARN] Imageset missing: {imageset_dir}") + return + + palette_images = { + "light": theme.get("light", {}).get("images", {}), + "dark": theme.get("dark", {}).get("images", {}), + } + + light_rel = palette_images["light"].get(task.kind) + dark_rel = palette_images["dark"].get(task.kind) if task.include_dark else None + if not light_rel and dark_rel: + light_rel = dark_rel + if task.include_dark and not dark_rel: + dark_rel = palette_images["light"].get(task.kind) + + if not light_rel: + print(f"[WARN] No image reference for {task.description} in theme.json") + return + + if not dry_run: + imageset_dir.mkdir(parents=True, exist_ok=True) + clean_imageset(imageset_dir, dry_run) + + light_name = copy_image(theme_dir, light_rel, imageset_dir, dry_run) + dark_name = copy_image(theme_dir, dark_rel, imageset_dir, dry_run) if dark_rel else None + if task.include_dark and not dark_name: + dark_name = light_name + + data = prepare_contents_json(contents_path) + images = [] + if light_name: + images.append({"idiom": "universal", "filename": light_name}) + if task.include_dark and dark_name: + images.append({ + "idiom": "universal", + "appearances": [{"appearance": "luminosity", "value": "dark"}], + "filename": dark_name, + }) + data["images"] = images + + if not dry_run: + with contents_path.open("w", encoding="utf-8") as fh: + json.dump(data, fh, indent=2, ensure_ascii=False) + fh.write("\n") + status = "[OK]" + else: + status = "[DRY-RUN]" + msg_parts = [f"light={light_name}"] + if task.include_dark: + msg_parts.append(f"dark={dark_name}") + print(f"{status} {task.description}: {', '.join(msg_parts)} -> {imageset_dir}") + + +def apply_app_icon(theme: Dict, theme_dir: Path, project_root: Path, dry_run: bool) -> None: + icon_rel = theme.get("icons", {}).get("appIcon") + if not icon_rel: + print("[WARN] No app icon path found in theme.json (icons.appIcon)") + return + iconset_dir = project_root / "OpenEdX/Assets.xcassets/AppIcon.appiconset" + contents_path = iconset_dir / "Contents.json" + if not iconset_dir.exists(): + print(f"[WARN] AppIcon.appiconset not found at {iconset_dir}") + return + + if not dry_run: + clean_imageset(iconset_dir, dry_run) + icon_name = copy_image(theme_dir, icon_rel, iconset_dir, dry_run) + if not icon_name: + return + + data = prepare_contents_json(contents_path) + data["images"] = [{ + "idiom": "universal", + "platform": "ios", + "size": "1024x1024", + "filename": icon_name, + }] + if not dry_run: + with contents_path.open("w", encoding="utf-8") as fh: + json.dump(data, fh, indent=2, ensure_ascii=False) + fh.write("\n") + status = "[OK]" + else: + status = "[DRY-RUN]" + print(f"{status} Updated app icon: {icon_name} -> {iconset_dir}") + + +def sanitize_bundle_component(theme_name: str) -> str: + lowered = theme_name.lower() + # Replace whitespace with dots + lowered = re.sub(r"\s+", ".", lowered) + # Remove disallowed characters + lowered = re.sub(r"[^a-z0-9\.]+", "", lowered) + # Collapse multiple dots and trim + lowered = re.sub(r"\.+", ".", lowered).strip('.') + return lowered or "app" + + +def update_project_display_and_bundle(theme_name: str, project_root: Path, dry_run: bool) -> None: + project_file = project_root / PROJECT_FILE_PATH + with project_file.open("r", encoding="utf-8") as fh: + lines = fh.readlines() + + env_map = { + "DebugProd": "prod", + "ReleaseProd": "prod", + "DebugStage": "stage", + "ReleaseStage": "stage", + "DebugDev": "dev", + "ReleaseDev": "dev", + } + + current_env: Optional[str] = None + brace_depth = 0 + changed = False + + def formatted_name(env: str) -> str: + suffix = ENV_DISPLAY_SUFFIX[env] + return f"{theme_name}{suffix}".strip() + + bundle_component = sanitize_bundle_component(theme_name) + + for idx, line in enumerate(lines): + if "/*" in line and "*/ = {" in line: + start = line.find("/*") + 2 + end = line.find("*/", start) + if end != -1: + config_name = line[start:end].strip() + current_env = env_map.get(config_name) + brace_depth = 1 if current_env else 0 + continue + + if current_env: + brace_depth += line.count("{") - line.count("}") + if "INFOPLIST_KEY_CFBundleDisplayName" in line: + new_value = formatted_name(current_env) + new_line = f"\t\t\tINFOPLIST_KEY_CFBundleDisplayName = \"{new_value}\";\n" + if lines[idx] != new_line: + lines[idx] = new_line + changed = True + elif "PRODUCT_BUNDLE_IDENTIFIER" in line: + parts = line.split("=") + if len(parts) >= 2: + current_value = parts[1].strip().rstrip(';') + current_value = current_value.strip('"') + tokens = current_value.split('.') if current_value else [] + if tokens: + target_index: Optional[int] = None + for i, token in enumerate(tokens): + if token.lower() == "openedx": + target_index = i + break + if target_index is None: + target_index = 1 if len(tokens) > 1 else 0 + slug_parts = [part for part in bundle_component.split('.') if part] + if not slug_parts: + slug_parts = [bundle_component] + prefix = tokens[:target_index] + suffix = tokens[target_index + 1:] + while slug_parts and suffix and slug_parts[-1] == suffix[0]: + suffix = suffix[1:] + new_tokens = [*prefix, *slug_parts, *suffix] + new_value = '.'.join(filter(None, new_tokens)) or bundle_component + if new_value != current_value: + quote = '"' if '"' in parts[1] else '' + lines[idx] = f"\t\t\tPRODUCT_BUNDLE_IDENTIFIER = {quote}{new_value}{quote};\n" + changed = True + elif brace_depth <= 0: + current_env = None + brace_depth = 0 + + if changed: + if dry_run: + print("[DRY-RUN] Would update display names/bundle IDs in project.pbxproj") + else: + with project_file.open("w", encoding="utf-8") as fh: + fh.writelines(lines) + print("[OK] Updated display names/bundle IDs in project.pbxproj") + else: + print("[INFO] Display names and bundle IDs already match theme") + + +def main() -> int: + args = parse_args() + try: + theme_dir = resolve_theme_dir(args.theme) + project_root = resolve_project_root(theme_dir, args.project) + ensure_project(project_root) + theme = load_theme(theme_dir) + except Exception as exc: + print(f"[ERROR] {exc}") + return 1 + + print(f"[INFO] Theme folder: {theme_dir}") + print(f"[INFO] iOS project folder: {project_root}") + if args.dry_run: + print("[INFO] Dry run enabled – no files will be modified.") + + palette = { + "light": theme.get("light", {}).get("colors", {}), + "dark": theme.get("dark", {}).get("colors", {}), + } + + for task in COLOR_TASKS: + apply_color_asset(palette, project_root, task, args.dry_run) + + for task in IMAGE_TASKS: + apply_image_asset(theme, theme_dir, project_root, task, args.dry_run) + + apply_app_icon(theme, theme_dir, project_root, args.dry_run) + + theme_name = theme.get("name") or "Open edX" + update_project_display_and_bundle(theme_name, project_root, args.dry_run) + + print("[DONE] Theme application complete") + return 0 + + +if __name__ == "__main__": # pragma: no cover + sys.exit(main()) diff --git a/Sandbox (3)/editor.json b/Sandbox (3)/editor.json new file mode 100644 index 000000000..7a6b3cee2 --- /dev/null +++ b/Sandbox (3)/editor.json @@ -0,0 +1,83 @@ +{ + "headerBg": { + "light": { + "type": "gradient", + "gradient": { + "top": "#b82669", + "bottom": "#22358c", + "reverse": false + }, + "image": { + "source": "catalog", + "path": "assets/header-light.png", + "file": null, + "scale": 1, + "offsetY": 0, + "tint": null, + "tintOpacity": 0 + } + }, + "dark": { + "type": "gradient", + "gradient": { + "top": "#b82669", + "bottom": "#22358c", + "reverse": false + }, + "image": { + "source": "catalog", + "path": "assets/header-dark.png", + "file": null, + "scale": 1, + "offsetY": 0, + "tint": null, + "tintOpacity": 0 + } + } + }, + "appIconOptions": { + "useBackground": true, + "bgColor": "#cc3366", + "makeWhite": true, + "scale": 1.2, + "androidScale": 1 + }, + "logoOptions": { + "loginScale": { + "light": 1, + "dark": 1 + }, + "makeWhite": { + "light": false, + "dark": false + } + }, + "splashPad": { + "enabled": { + "light": false, + "dark": false + }, + "color": { + "light": null, + "dark": null + }, + "scale": { + "light": 1, + "dark": 1 + } + }, + "splashLogoOptions": { + "makeWhite": { + "light": false, + "dark": false + }, + "squareIcon": { + "light": true, + "dark": true + } + }, + "unify": { + "appLogoLogin": false, + "appLogoSplash": false + } +} \ No newline at end of file diff --git a/Sandbox (3)/images/android/drawable-night/app_splash_icon.png b/Sandbox (3)/images/android/drawable-night/app_splash_icon.png new file mode 100644 index 000000000..6130d487c Binary files /dev/null and b/Sandbox (3)/images/android/drawable-night/app_splash_icon.png differ diff --git a/Sandbox (3)/images/android/drawable-night/core_ic_logo.png b/Sandbox (3)/images/android/drawable-night/core_ic_logo.png new file mode 100644 index 000000000..963453a36 Binary files /dev/null and b/Sandbox (3)/images/android/drawable-night/core_ic_logo.png differ diff --git a/Sandbox (3)/images/android/drawable/app_splash_icon.png b/Sandbox (3)/images/android/drawable/app_splash_icon.png new file mode 100644 index 000000000..6ff781603 Binary files /dev/null and b/Sandbox (3)/images/android/drawable/app_splash_icon.png differ diff --git a/Sandbox (3)/images/android/drawable/core_ic_logo.png b/Sandbox (3)/images/android/drawable/core_ic_logo.png new file mode 100644 index 000000000..f9074f18a Binary files /dev/null and b/Sandbox (3)/images/android/drawable/core_ic_logo.png differ diff --git a/Sandbox (3)/images/android/ic_launcher-playstore.png b/Sandbox (3)/images/android/ic_launcher-playstore.png new file mode 100644 index 000000000..6a5ec4302 Binary files /dev/null and b/Sandbox (3)/images/android/ic_launcher-playstore.png differ diff --git a/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher.webp b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher.webp new file mode 100644 index 000000000..428ce5fd3 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher.webp differ diff --git a/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_background.webp b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_background.webp new file mode 100644 index 000000000..2089b4d24 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_background.webp differ diff --git a/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_foreground.webp b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..518e1b3c2 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_foreground.webp differ diff --git a/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_round.webp b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_round.webp new file mode 100644 index 000000000..0d7eb0b93 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-hdpi/ic_launcher_round.webp differ diff --git a/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher.webp b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher.webp new file mode 100644 index 000000000..ec3b97f0f Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher.webp differ diff --git a/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_background.webp b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_background.webp new file mode 100644 index 000000000..01a3832ed Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_background.webp differ diff --git a/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_foreground.webp b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..d3c2e4dae Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_foreground.webp differ diff --git a/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_round.webp b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_round.webp new file mode 100644 index 000000000..21adfc4e0 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-mdpi/ic_launcher_round.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher.webp b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher.webp new file mode 100644 index 000000000..a8df8c717 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_background.webp b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_background.webp new file mode 100644 index 000000000..ca607745e Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_background.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_foreground.webp b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..865556056 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_foreground.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_round.webp b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..a01c04da8 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xhdpi/ic_launcher_round.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher.webp b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher.webp new file mode 100644 index 000000000..598e51f13 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_background.webp b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_background.webp new file mode 100644 index 000000000..f83b0a338 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_background.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_foreground.webp b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..a546f151d Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_foreground.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_round.webp b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..51a20bc54 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxhdpi/ic_launcher_round.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher.webp b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher.webp new file mode 100644 index 000000000..796c41f69 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_background.webp b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_background.webp new file mode 100644 index 000000000..e42a798a5 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_background.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_foreground.webp b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_foreground.webp new file mode 100644 index 000000000..33b67c8aa Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_foreground.webp differ diff --git a/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_round.webp b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_round.webp new file mode 100644 index 000000000..7777af017 Binary files /dev/null and b/Sandbox (3)/images/android/mipmap-xxxhdpi/ic_launcher_round.webp differ diff --git a/Sandbox (3)/images/app_icon_ios.png b/Sandbox (3)/images/app_icon_ios.png new file mode 100644 index 000000000..6fed09d2d Binary files /dev/null and b/Sandbox (3)/images/app_icon_ios.png differ diff --git a/Sandbox (3)/images/applogologin_dark.png b/Sandbox (3)/images/applogologin_dark.png new file mode 100644 index 000000000..a7201c386 Binary files /dev/null and b/Sandbox (3)/images/applogologin_dark.png differ diff --git a/Sandbox (3)/images/applogologin_light.png b/Sandbox (3)/images/applogologin_light.png new file mode 100644 index 000000000..9e1e17b22 --- /dev/null +++ b/Sandbox (3)/images/applogologin_light.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sandbox (3)/images/applogosplash_dark.png b/Sandbox (3)/images/applogosplash_dark.png new file mode 100644 index 000000000..a7201c386 Binary files /dev/null and b/Sandbox (3)/images/applogosplash_dark.png differ diff --git a/Sandbox (3)/images/applogosplash_light.png b/Sandbox (3)/images/applogosplash_light.png new file mode 100644 index 000000000..9e1e17b22 --- /dev/null +++ b/Sandbox (3)/images/applogosplash_light.png @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/Sandbox (3)/images/header_background_dark.png b/Sandbox (3)/images/header_background_dark.png new file mode 100644 index 000000000..d1e413bc3 Binary files /dev/null and b/Sandbox (3)/images/header_background_dark.png differ diff --git a/Sandbox (3)/images/header_background_light.png b/Sandbox (3)/images/header_background_light.png new file mode 100644 index 000000000..d1e413bc3 Binary files /dev/null and b/Sandbox (3)/images/header_background_light.png differ diff --git a/Sandbox (3)/theme.json b/Sandbox (3)/theme.json new file mode 100644 index 000000000..929a2e0f8 --- /dev/null +++ b/Sandbox (3)/theme.json @@ -0,0 +1,100 @@ +{ + "name": "Sandbox", + "light": { + "colors": { + "accentColor": "#cc3366", + "background": "#FFFFFF", + "textPrimary": "#19212F", + "textSecondary": "#97A5BB", + "infoColor": "#cc3366", + "textInputBackground": "#FFFFFF", + "textInputStroke": "#97A5BB", + "primaryButtonBg": "#cc3366", + "primaryButtonText": "#FFFFFF", + "secondaryButtonBorder": "#cc3366", + "secondaryButtonText": "#cc3366", + "tabbarBGColor": "#F8F9FA", + "tabbarInactiveColor": "#97A5BB", + "tabbarActiveColor": "#cc3366" + }, + "images": { + "headerBackground": "images/header_background_light.png", + "appLogoLogin": "images/applogologin_light.png", + "appLogoSplash": "images/applogosplash_light.png" + }, + "imagesMeta": { + "appLogoLogin": { + "width": 171, + "height": 48.45 + } + } + }, + "dark": { + "colors": { + "accentColor": "#AD1457", + "background": "#18202E", + "textPrimary": "#FFFFFF", + "textSecondary": "#79889F", + "infoColor": "#AD1457", + "textInputBackground": "#273346", + "textInputStroke": "#4E5A70", + "primaryButtonBg": "#AD1457", + "primaryButtonText": "#FFFFFF", + "secondaryButtonBorder": "#AD1457", + "secondaryButtonText": "#AD1457", + "tabbarBGColor": "#273346", + "tabbarInactiveColor": "#79889F", + "tabbarActiveColor": "#AD1457" + }, + "images": { + "headerBackground": "images/header_background_dark.png", + "appLogoLogin": "images/applogologin_dark.png", + "appLogoSplash": "images/applogosplash_dark.png" + }, + "imagesMeta": { + "appLogoLogin": { + "width": 171, + "height": 49.38 + } + } + }, + "icons": { + "appIcon": "images/app_icon_ios.png", + "appIconAndroid": { + "backgroundColor": "#cc3366", + "splashColorLight": "#cc3366", + "splashColorDark": "#cc3366", + "useBackground": true, + "assets": { + "mipmap-mdpi/ic_launcher.webp": "images/android/mipmap-mdpi/ic_launcher.webp", + "mipmap-mdpi/ic_launcher_round.webp": "images/android/mipmap-mdpi/ic_launcher_round.webp", + "mipmap-hdpi/ic_launcher.webp": "images/android/mipmap-hdpi/ic_launcher.webp", + "mipmap-hdpi/ic_launcher_round.webp": "images/android/mipmap-hdpi/ic_launcher_round.webp", + "mipmap-xhdpi/ic_launcher.webp": "images/android/mipmap-xhdpi/ic_launcher.webp", + "mipmap-xhdpi/ic_launcher_round.webp": "images/android/mipmap-xhdpi/ic_launcher_round.webp", + "mipmap-xxhdpi/ic_launcher.webp": "images/android/mipmap-xxhdpi/ic_launcher.webp", + "mipmap-xxhdpi/ic_launcher_round.webp": "images/android/mipmap-xxhdpi/ic_launcher_round.webp", + "mipmap-xxxhdpi/ic_launcher.webp": "images/android/mipmap-xxxhdpi/ic_launcher.webp", + "mipmap-xxxhdpi/ic_launcher_round.webp": "images/android/mipmap-xxxhdpi/ic_launcher_round.webp", + "mipmap-mdpi/ic_launcher_foreground.webp": "images/android/mipmap-mdpi/ic_launcher_foreground.webp", + "mipmap-hdpi/ic_launcher_foreground.webp": "images/android/mipmap-hdpi/ic_launcher_foreground.webp", + "mipmap-xhdpi/ic_launcher_foreground.webp": "images/android/mipmap-xhdpi/ic_launcher_foreground.webp", + "mipmap-xxhdpi/ic_launcher_foreground.webp": "images/android/mipmap-xxhdpi/ic_launcher_foreground.webp", + "mipmap-xxxhdpi/ic_launcher_foreground.webp": "images/android/mipmap-xxxhdpi/ic_launcher_foreground.webp", + "mipmap-mdpi/ic_launcher_background.webp": "images/android/mipmap-mdpi/ic_launcher_background.webp", + "mipmap-hdpi/ic_launcher_background.webp": "images/android/mipmap-hdpi/ic_launcher_background.webp", + "mipmap-xhdpi/ic_launcher_background.webp": "images/android/mipmap-xhdpi/ic_launcher_background.webp", + "mipmap-xxhdpi/ic_launcher_background.webp": "images/android/mipmap-xxhdpi/ic_launcher_background.webp", + "mipmap-xxxhdpi/ic_launcher_background.webp": "images/android/mipmap-xxxhdpi/ic_launcher_background.webp", + "drawable/app_splash_icon.png": "images/android/drawable/app_splash_icon.png", + "drawable-night/app_splash_icon.png": "images/android/drawable-night/app_splash_icon.png" + }, + "playStore": "images/android/ic_launcher-playstore.png", + "scale": 1 + } + }, + "shapes": { + "rounded": true, + "buttonRadius": 8 + } +} \ No newline at end of file diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json index 06ab94dff..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentButtonColor.colorset/Contents.json @@ -7,7 +7,7 @@ "alpha" : "1.000", "blue" : "0xFF", "green" : "0x68", - "red" : "0x3B" + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json index bf1a96417..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0x78", - "red" : "0x53" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json index bf1a96417..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/AccentXColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0x78", - "red" : "0x53" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json index df1a5f141..2139b2192 100644 --- a/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/Background.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "1.000", + "green": "1.000", + "red": "1.000" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0x2E", - "green" : "0x20", - "red" : "0x18" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.18", + "green": "0.125", + "red": "0.094" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/DeleteAccountBG.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/DeleteAccountBG.colorset/Contents.json index bf1a96417..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/DeleteAccountBG.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/DeleteAccountBG.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0x78", - "red" : "0x53" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset/Contents.json index 00d59cb46..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/InfoColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json index 8fef18d07..2139b2192 100644 --- a/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/LoginBackground.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "1.000", + "green": "1.000", + "red": "1.000" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.184", - "green" : "0.129", - "red" : "0.098" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.18", + "green": "0.125", + "red": "0.094" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/PrimaryButtonTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/PrimaryButtonTextColor.colorset/Contents.json index 22c4bb0a8..ca5ba7c1a 100644 --- a/Theme/Theme/Assets.xcassets/Colors/PrimaryButtonTextColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/PrimaryButtonTextColor.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "1.000", + "green": "1.000", + "red": "1.000" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "1.000", + "green": "1.000", + "red": "1.000" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/ResumeButton/ResumeButtonBG.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/ResumeButton/ResumeButtonBG.colorset/Contents.json index bf1a96417..87c092aa4 100644 --- a/Theme/Theme/Assets.xcassets/Colors/ResumeButton/ResumeButtonBG.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/ResumeButton/ResumeButtonBG.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.4", + "green": "0.2", + "red": "0.8" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0x78", - "red" : "0x53" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.341", + "green": "0.078", + "red": "0.678" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json index 00d59cb46..87c092aa4 100644 --- a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonBorderColor.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.4", + "green": "0.2", + "red": "0.8" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.341", + "green": "0.078", + "red": "0.678" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json index 00d59cb46..87c092aa4 100644 --- a/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/SecondaryButton/SecondaryButtonTextColor.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.4", + "green": "0.2", + "red": "0.8" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.341", + "green": "0.078", + "red": "0.678" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json index 9151445b8..378733378 100644 --- a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingStrokeColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json index 93c1c4d85..a46407471 100644 --- a/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/SlidingTabBar/slidingTextColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/SocialAuthColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/SocialAuthColor.colorset/Contents.json index bf1a96417..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/SocialAuthColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/SocialAuthColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0xF8", - "green" : "0x78", - "red" : "0x53" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json index a3f0c654a..b2e4620e4 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextPrimary.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.184", - "green" : "0.129", - "red" : "0.098" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.184", + "green": "0.129", + "red": "0.098" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "1.000", + "green": "1.000", + "red": "1.000" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json index 93691d3e8..1ea824edb 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TextColor/TextSecondary.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.733", - "green" : "0.647", - "red" : "0.592" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.733", + "green": "0.647", + "red": "0.592" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.624", - "green" : "0.533", - "red" : "0.475" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.624", + "green": "0.533", + "red": "0.475" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json index 164b36790..5aa782761 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputBackground.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "1.000", - "green" : "1.000", - "red" : "1.000" + "color": { + "color-space": "display-p3", + "components": { + "alpha": "1.000", + "blue": "1.000", + "green": "1.000", + "red": "1.000" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "display-p3", - "components" : { - "alpha" : "1.000", - "blue" : "0.275", - "green" : "0.200", - "red" : "0.153" + "color": { + "color-space": "display-p3", + "components": { + "alpha": "1.000", + "blue": "0.275", + "green": "0.2", + "red": "0.153" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json index 5cd29db93..3e4aee938 100644 --- a/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/TextInput/TextInputStroke.colorset/Contents.json @@ -1,38 +1,38 @@ { - "colors" : [ + "colors": [ { - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.733", - "green" : "0.647", - "red" : "0.592" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.733", + "green": "0.647", + "red": "0.592" } }, - "idiom" : "universal" + "idiom": "universal" }, { - "appearances" : [ + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "color" : { - "color-space" : "srgb", - "components" : { - "alpha" : "1.000", - "blue" : "0.439", - "green" : "0.353", - "red" : "0.306" + "color": { + "color-space": "srgb", + "components": { + "alpha": "1.000", + "blue": "0.439", + "green": "0.353", + "red": "0.306" } }, - "idiom" : "universal" + "idiom": "universal" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/Colors/ToggleSwitchColor.colorset/Contents.json b/Theme/Theme/Assets.xcassets/Colors/ToggleSwitchColor.colorset/Contents.json index 00d59cb46..1ffc23315 100644 --- a/Theme/Theme/Assets.xcassets/Colors/ToggleSwitchColor.colorset/Contents.json +++ b/Theme/Theme/Assets.xcassets/Colors/ToggleSwitchColor.colorset/Contents.json @@ -5,9 +5,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "1.000", - "green" : "0.408", - "red" : "0.235" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" @@ -23,9 +23,9 @@ "color-space" : "srgb", "components" : { "alpha" : "1.000", - "blue" : "0.976", - "green" : "0.471", - "red" : "0.329" + "blue" : "0xFF", + "green" : "0x68", + "red" : "0x3C" } }, "idiom" : "universal" diff --git a/Theme/Theme/Assets.xcassets/appLogo.imageset/Contents.json b/Theme/Theme/Assets.xcassets/appLogo.imageset/Contents.json index b72b1ac1e..b9787c07a 100644 --- a/Theme/Theme/Assets.xcassets/appLogo.imageset/Contents.json +++ b/Theme/Theme/Assets.xcassets/appLogo.imageset/Contents.json @@ -1,7 +1,17 @@ { "images" : [ { - "filename" : "Frame 4 1.svg", + "filename" : "applogologin_light.png", + "idiom" : "universal" + }, + { + "appearances" : [ + { + "appearance" : "luminosity", + "value" : "dark" + } + ], + "filename" : "applogologin_light 1.png", "idiom" : "universal" } ], diff --git a/Theme/Theme/Assets.xcassets/appLogo.imageset/Frame 4 1.svg b/Theme/Theme/Assets.xcassets/appLogo.imageset/Frame 4 1.svg deleted file mode 100644 index 5a2d136db..000000000 --- a/Theme/Theme/Assets.xcassets/appLogo.imageset/Frame 4 1.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/Theme/Theme/Assets.xcassets/appLogo.imageset/applogologin_light 1.png b/Theme/Theme/Assets.xcassets/appLogo.imageset/applogologin_light 1.png new file mode 100644 index 000000000..483e948b2 Binary files /dev/null and b/Theme/Theme/Assets.xcassets/appLogo.imageset/applogologin_light 1.png differ diff --git a/Theme/Theme/Assets.xcassets/appLogo.imageset/applogologin_light.png b/Theme/Theme/Assets.xcassets/appLogo.imageset/applogologin_light.png new file mode 100644 index 000000000..483e948b2 Binary files /dev/null and b/Theme/Theme/Assets.xcassets/appLogo.imageset/applogologin_light.png differ diff --git a/Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json index a58b5e5c1..57a40826c 100644 --- a/Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json +++ b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Contents.json @@ -1,22 +1,22 @@ { - "images" : [ + "images": [ { - "filename" : "Rectangle.png", - "idiom" : "universal" + "idiom": "universal", + "filename": "header_background_light.png" }, { - "appearances" : [ + "idiom": "universal", + "appearances": [ { - "appearance" : "luminosity", - "value" : "dark" + "appearance": "luminosity", + "value": "dark" } ], - "filename" : "Rectangle-2.png", - "idiom" : "universal" + "filename": "header_background_dark.png" } ], - "info" : { - "author" : "xcode", - "version" : 1 + "info": { + "author": "xcode", + "version": 1 } } diff --git a/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png deleted file mode 100644 index 9473b37ad..000000000 Binary files a/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle-2.png and /dev/null differ diff --git a/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png deleted file mode 100644 index 5e7705937..000000000 Binary files a/Theme/Theme/Assets.xcassets/headerBackground.imageset/Rectangle.png and /dev/null differ diff --git a/Theme/Theme/Assets.xcassets/headerBackground.imageset/header_background_dark.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/header_background_dark.png new file mode 100644 index 000000000..db7747c67 Binary files /dev/null and b/Theme/Theme/Assets.xcassets/headerBackground.imageset/header_background_dark.png differ diff --git a/Theme/Theme/Assets.xcassets/headerBackground.imageset/header_background_light.png b/Theme/Theme/Assets.xcassets/headerBackground.imageset/header_background_light.png new file mode 100644 index 000000000..5f2d5c2c8 Binary files /dev/null and b/Theme/Theme/Assets.xcassets/headerBackground.imageset/header_background_light.png differ diff --git a/default_config/prod/config.yaml b/default_config/prod/config.yaml index e75c30495..835e70664 100644 --- a/default_config/prod/config.yaml +++ b/default_config/prod/config.yaml @@ -1,9 +1,9 @@ -API_HOST_URL: 'http://localhost:8000' +API_HOST_URL: 'https://sandbox.openedx.org' SSO_URL: 'http://localhost:8000' SSO_FINISHED_URL: 'http://localhost:8000' ENVIRONMENT_DISPLAY_NAME: 'Localhost' FEEDBACK_EMAIL_ADDRESS: 'support@example.com' -OAUTH_CLIENT_ID: '' +OAUTH_CLIENT_ID: 'ios' SSO_BUTTON_TITLE: ar: "الدخول عبر SSO" @@ -14,6 +14,19 @@ APP_LEVEL_DATES_ENABLED: false EXPERIMENTAL_FEATURES: APP_LEVEL_DOWNLOADS: ENABLED: false + +FIREBASE: + ENABLED: true + ANALYTICS_SOURCE: "firebase" + CLOUD_MESSAGING_ENABLED: false + API_KEY: "AIzaSyCKAIXDLM7pnX43P_viTsfgbxrLBOaJwGo" + BUNDLE_ID: "com.raccoongang.NewEdX.stage" + CLIENT_ID: "156114692773-r5pgdcdjqq7sup75fdla4lk3q3kjc6m8.apps.googleusercontent.com" + GCM_SENDER_ID: "156114692773" + GOOGLE_APP_ID: "1:156114692773:ios:8058bca851a8bc7c187b4c" + PROJECT_ID: "openedxmobile-stage" + REVERSED_CLIENT_ID: "com.googleusercontent.apps.156114692773-r5pgdcdjqq7sup75fdla4lk3q3kjc6m8" + STORAGE_BUCKET: "openedxmobile-stage.appspot.com" UI_COMPONENTS: