diff --git a/Dreamio.xcodeproj/project.pbxproj b/Dreamio.xcodeproj/project.pbxproj new file mode 100644 index 0000000..5324de1 --- /dev/null +++ b/Dreamio.xcodeproj/project.pbxproj @@ -0,0 +1,319 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 56; + objects = { + +/* Begin PBXBuildFile section */ + 6F2A2B362C00100100DREAMIO /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2A2B332C00100100DREAMIO /* AppDelegate.swift */; }; + 6F2A2B372C00100100DREAMIO /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2A2B342C00100100DREAMIO /* SceneDelegate.swift */; }; + 6F2A2B382C00100100DREAMIO /* DreamioWebViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6F2A2B352C00100100DREAMIO /* DreamioWebViewController.swift */; }; +/* End PBXBuildFile section */ + +/* Begin PBXFileReference section */ + 6F2A2B302C00100100DREAMIO /* Dreamio.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Dreamio.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 6F2A2B332C00100100DREAMIO /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 6F2A2B342C00100100DREAMIO /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; + 6F2A2B352C00100100DREAMIO /* DreamioWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DreamioWebViewController.swift; sourceTree = ""; }; + 6F2A2B392C00100100DREAMIO /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; +/* End PBXFileReference section */ + +/* Begin PBXFrameworksBuildPhase section */ + 6F2A2B2D2C00100100DREAMIO /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + 6F2A2B272C00100100DREAMIO = { + isa = PBXGroup; + children = ( + 6F2A2B322C00100100DREAMIO /* Dreamio */, + 6F2A2B312C00100100DREAMIO /* Products */, + ); + sourceTree = ""; + }; + 6F2A2B312C00100100DREAMIO /* Products */ = { + isa = PBXGroup; + children = ( + 6F2A2B302C00100100DREAMIO /* Dreamio.app */, + ); + name = Products; + sourceTree = ""; + }; + 6F2A2B322C00100100DREAMIO /* Dreamio */ = { + isa = PBXGroup; + children = ( + 6F2A2B332C00100100DREAMIO /* AppDelegate.swift */, + 6F2A2B342C00100100DREAMIO /* SceneDelegate.swift */, + 6F2A2B352C00100100DREAMIO /* DreamioWebViewController.swift */, + 6F2A2B392C00100100DREAMIO /* Info.plist */, + ); + path = Dreamio; + sourceTree = ""; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 6F2A2B2F2C00100100DREAMIO /* Dreamio */ = { + isa = PBXNativeTarget; + buildConfigurationList = 6F2A2B412C00100100DREAMIO /* Build configuration list for PBXNativeTarget "Dreamio" */; + buildPhases = ( + 6F2A2B2C2C00100100DREAMIO /* Sources */, + 6F2A2B2D2C00100100DREAMIO /* Frameworks */, + 6F2A2B2E2C00100100DREAMIO /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + name = Dreamio; + productName = Dreamio; + productReference = 6F2A2B302C00100100DREAMIO /* Dreamio.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 6F2A2B282C00100100DREAMIO /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1600; + LastUpgradeCheck = 1600; + TargetAttributes = { + 6F2A2B2F2C00100100DREAMIO = { + CreatedOnToolsVersion = 16.0; + }; + }; + }; + buildConfigurationList = 6F2A2B2B2C00100100DREAMIO /* Build configuration list for PBXProject "Dreamio" */; + compatibilityVersion = "Xcode 14.0"; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = 6F2A2B272C00100100DREAMIO; + productRefGroup = 6F2A2B312C00100100DREAMIO /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 6F2A2B2F2C00100100DREAMIO /* Dreamio */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + 6F2A2B2E2C00100100DREAMIO /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 6F2A2B2C2C00100100DREAMIO /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 6F2A2B362C00100100DREAMIO /* AppDelegate.swift in Sources */, + 6F2A2B372C00100100DREAMIO /* SceneDelegate.swift in Sources */, + 6F2A2B382C00100100DREAMIO /* DreamioWebViewController.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + 6F2A2B3A2C00100100DREAMIO /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 6F2A2B3B2C00100100DREAMIO /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 6F2A2B3E2C00100100DREAMIO /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Dreamio/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kell.dreamio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6F2A2B3F2C00100100DREAMIO /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + DEVELOPMENT_TEAM = ""; + GENERATE_INFOPLIST_FILE = NO; + INFOPLIST_FILE = Dreamio/Info.plist; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + MARKETING_VERSION = 0.1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.kell.dreamio; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + 6F2A2B2B2C00100100DREAMIO /* Build configuration list for PBXProject "Dreamio" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6F2A2B3A2C00100100DREAMIO /* Debug */, + 6F2A2B3B2C00100100DREAMIO /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + 6F2A2B412C00100100DREAMIO /* Build configuration list for PBXNativeTarget "Dreamio" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 6F2A2B3E2C00100100DREAMIO /* Debug */, + 6F2A2B3F2C00100100DREAMIO /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 6F2A2B282C00100100DREAMIO /* Project object */; +} diff --git a/Dreamio/AppDelegate.swift b/Dreamio/AppDelegate.swift new file mode 100644 index 0000000..66854de --- /dev/null +++ b/Dreamio/AppDelegate.swift @@ -0,0 +1,19 @@ +import UIKit + +@main +final class AppDelegate: UIResponder, UIApplicationDelegate { + func application( + _ application: UIApplication, + didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? + ) -> Bool { + true + } + + func application( + _ application: UIApplication, + configurationForConnecting connectingSceneSession: UISceneSession, + options: UIScene.ConnectionOptions + ) -> UISceneConfiguration { + UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) + } +} diff --git a/Dreamio/DreamioWebViewController.swift b/Dreamio/DreamioWebViewController.swift new file mode 100644 index 0000000..6d0376a --- /dev/null +++ b/Dreamio/DreamioWebViewController.swift @@ -0,0 +1,140 @@ +import UIKit +import WebKit + +final class DreamioWebViewController: UIViewController { + private enum Constants { + static let stremioWebURL = URL(string: "https://web.stremio.com/")! + } + + private lazy var webView: WKWebView = { + let configuration = WKWebViewConfiguration() + configuration.defaultWebpagePreferences.allowsContentJavaScript = true + configuration.allowsInlineMediaPlayback = true + configuration.mediaTypesRequiringUserActionForPlayback = [] + configuration.preferences.javaScriptCanOpenWindowsAutomatically = true + + let webView = WKWebView(frame: .zero, configuration: configuration) + webView.translatesAutoresizingMaskIntoConstraints = false + webView.allowsBackForwardNavigationGestures = true + webView.customUserAgent = "Dreamio/0.1 WKWebView" + webView.navigationDelegate = self + webView.uiDelegate = self + webView.scrollView.contentInsetAdjustmentBehavior = .never + return webView + }() + + private let progressView: UIProgressView = { + let view = UIProgressView(progressViewStyle: .bar) + view.translatesAutoresizingMaskIntoConstraints = false + view.tintColor = UIColor(red: 0.55, green: 0.35, blue: 0.95, alpha: 1.0) + return view + }() + + private var progressObservation: NSKeyValueObservation? + + override func viewDidLoad() { + super.viewDidLoad() + + view.backgroundColor = .systemBackground + view.addSubview(webView) + view.addSubview(progressView) + + NSLayoutConstraint.activate([ + webView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + webView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + webView.topAnchor.constraint(equalTo: view.topAnchor), + webView.bottomAnchor.constraint(equalTo: view.bottomAnchor), + progressView.leadingAnchor.constraint(equalTo: view.leadingAnchor), + progressView.trailingAnchor.constraint(equalTo: view.trailingAnchor), + progressView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor) + ]) + + progressObservation = webView.observe(\.estimatedProgress, options: [.new]) { [weak self] webView, _ in + self?.updateProgress(webView.estimatedProgress) + } + + loadDreamio() + } + + private func loadDreamio() { + let request = URLRequest(url: Constants.stremioWebURL) + webView.load(request) + } + + private func updateProgress(_ progress: Double) { + progressView.isHidden = progress >= 1.0 + progressView.setProgress(Float(progress), animated: true) + } + + private func showLoadFailure(_ error: Error) { + let alert = UIAlertController( + title: "Could not load Dreamio", + message: error.localizedDescription, + preferredStyle: .alert + ) + alert.addAction(UIAlertAction(title: "Retry", style: .default) { [weak self] _ in + self?.loadDreamio() + }) + present(alert, animated: true) + } +} + +extension DreamioWebViewController: WKNavigationDelegate { + func webView( + _ webView: WKWebView, + decidePolicyFor navigationAction: WKNavigationAction, + decisionHandler: @escaping (WKNavigationActionPolicy) -> Void + ) { + guard let url = navigationAction.request.url else { + decisionHandler(.cancel) + return + } + + if shouldOpenExternally(url: url, navigationType: navigationAction.navigationType) { + UIApplication.shared.open(url) + decisionHandler(.cancel) + return + } + + decisionHandler(.allow) + } + + func webView(_ webView: WKWebView, didFail navigation: WKNavigation!, withError error: Error) { + showLoadFailure(error) + } + + func webView(_ webView: WKWebView, didFailProvisionalNavigation navigation: WKNavigation!, withError error: Error) { + showLoadFailure(error) + } + + private func shouldOpenExternally(url: URL, navigationType: WKNavigationType) -> Bool { + guard let scheme = url.scheme?.lowercased() else { + return false + } + + if ["http", "https"].contains(scheme) { + return false + } + + if ["mailto", "tel", "sms"].contains(scheme) { + return true + } + + return navigationType == .linkActivated + } +} + +extension DreamioWebViewController: WKUIDelegate { + func webView( + _ webView: WKWebView, + createWebViewWith configuration: WKWebViewConfiguration, + for navigationAction: WKNavigationAction, + windowFeatures: WKWindowFeatures + ) -> WKWebView? { + if navigationAction.targetFrame == nil { + webView.load(navigationAction.request) + } + + return nil + } +} diff --git a/Dreamio/Info.plist b/Dreamio/Info.plist new file mode 100644 index 0000000..9451a6a --- /dev/null +++ b/Dreamio/Info.plist @@ -0,0 +1,38 @@ + + + + + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneConfigurationName + Default Configuration + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + + + + + UILaunchScreen + + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + + diff --git a/Dreamio/SceneDelegate.swift b/Dreamio/SceneDelegate.swift new file mode 100644 index 0000000..2781eac --- /dev/null +++ b/Dreamio/SceneDelegate.swift @@ -0,0 +1,20 @@ +import UIKit + +final class SceneDelegate: UIResponder, UIWindowSceneDelegate { + var window: UIWindow? + + func scene( + _ scene: UIScene, + willConnectTo session: UISceneSession, + options connectionOptions: UIScene.ConnectionOptions + ) { + guard let windowScene = scene as? UIWindowScene else { + return + } + + let window = UIWindow(windowScene: windowScene) + window.rootViewController = DreamioWebViewController() + window.makeKeyAndVisible() + self.window = window + } +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..d18fe91 --- /dev/null +++ b/README.md @@ -0,0 +1,34 @@ +# Dreamio + +Dreamio is a minimal iOS `WKWebView` wrapper around hosted Stremio Web. + +The MVP intentionally keeps native code thin. It loads `https://web.stremio.com/` +inside a UIKit host app, handles new-window navigation in the existing web view, +allows inline media playback, and leaves playback viability to real-device +testing. + +## Running the MVP + +1. Open `Dreamio.xcodeproj` in Xcode. +2. Select the `Dreamio` scheme. +3. Pick a real iPhone or iPad device. +4. Set a development team for code signing if Xcode asks. +5. Build and run. + +The repository machine currently has Command Line Tools selected instead of full +Xcode, so command-line `xcodebuild` validation is not available here. + +## MVP Validation Checklist + +- Cold launch loads hosted Stremio Web. +- Login completes and persists after app relaunch. +- Catalog and library navigation work. +- Addon install or configuration flows work, including redirects or popups. +- HLS direct stream playback works. +- MP4 direct stream playback works. +- Unsupported formats fail understandably. +- Fullscreen, rotation, pause/resume, and background/foreground behavior are + acceptable for v1. + +Track playback results by device, iOS version, stream protocol, container, +codec, subtitle type, HTTP status, and WebKit media error when available. diff --git a/docs/turns/2026-05-24-build-wkwebview-mvp-shell.html b/docs/turns/2026-05-24-build-wkwebview-mvp-shell.html new file mode 100644 index 0000000..6ed5185 --- /dev/null +++ b/docs/turns/2026-05-24-build-wkwebview-mvp-shell.html @@ -0,0 +1,260 @@ + + + + + + Dreamio WKWebView MVP Shell + + + + +
+
+
Repository implementation turn
+

Dreamio WKWebView MVP shell

+

Created the first runnable Dreamio app shape: a thin iOS UIKit host with a single WKWebView that loads hosted Stremio Web, handles popup-style navigation in place, allows inline media playback, and gives the user a concrete real-device validation checklist.

+
+ +
+

Summary

+

The repository moved from planning-only files to an MVP iOS app scaffold. The app is intentionally narrow: it exists to prove whether hosted Stremio Web can support login, browsing, addon flows, and direct playback inside WKWebView on real iPhone and iPad hardware.

+
+ +
+

Changes Made

+
    +
  • Added Dreamio.xcodeproj with a single iOS application target.
  • +
  • Added a UIKit lifecycle with AppDelegate and SceneDelegate.
  • +
  • Added DreamioWebViewController, which loads https://web.stremio.com/ in a configured WKWebView.
  • +
  • Enabled inline media playback, JavaScript window opening, back-forward gestures, and a small load progress indicator.
  • +
  • Added basic external URL handling and in-place handling for new-window flows.
  • +
  • Added README.md with run instructions and the MVP validation checklist.
  • +
+
+ +
+

Context

+

The source plan in /Users/kell/dreamio-plan.md recommends starting with hosted Stremio Web before bundling local assets. This implementation follows that gate exactly: no local Stremio Web fork, no native player bridge, no torrent engine, and no App Store positioning work.

+
+ +
+

Important Implementation Details

+
    +
  • The MVP URL is centralized in DreamioWebViewController.Constants.stremioWebURL.
  • +
  • WKUIDelegate loads targetless popup requests in the existing web view so auth or addon flows do not vanish.
  • +
  • Non-HTTP user-activated links such as mailto:, tel:, and sms: open externally.
  • +
  • Orientation support includes portrait and landscape on iPhone, plus upside-down portrait on iPad.
  • +
  • Code signing is left automatic with an empty development team, so Xcode can prompt for the correct personal or team signing identity.
  • +
+
+ +
+

Relevant Diff Snippets

+

The snippets below are rendered with Diffs when the document has network access, with plain-code fallback content retained in the page.

+
+
+++ Dreamio/DreamioWebViewController.swift
++ WKWebView configuration loads https://web.stremio.com/
++ Inline media playback is enabled.
++ Popup/new-window requests are loaded in the existing web view.
++ Basic load failure UI offers Retry.
+
+
+++ README.md
++ Added Xcode run instructions.
++ Added hosted web, login, addon, HLS, MP4, fullscreen, rotation, and relaunch validation checklist.
+
+ +
+

Expected Impact for End-Users

+

The user can now open the project in Xcode, install it on a real iOS device, and start testing whether Stremio Web is viable inside a private Dreamio shell. The app should feel like a focused wrapper, not a rewritten media application.

+
+ +
+

Validation

+
    +
  • Ran plutil -lint Dreamio/Info.plist: passed.
  • +
  • Ran plutil -lint Dreamio.xcodeproj/project.pbxproj: passed.
  • +
  • Checked local Swift availability with swift --version: available.
  • +
  • Attempted to check Xcode build availability with xcodebuild -version: blocked because the active developer directory is Command Line Tools, not full Xcode.
  • +
+
+ +
+

Issues, Limitations, and Mitigations

+
    +
  • The app has not been compiled on this machine because full Xcode is not selected. Mitigation: open the project in Xcode or switch xcode-select to a full Xcode install, then build on device.
  • +
  • No real-device playback gate has been passed yet. Mitigation: use the README checklist and record stream failures by protocol, container, codec, subtitles, HTTP status, and WebKit error.
  • +
  • Beads Dolt sync could not pull because no remote is configured. Mitigation: this is documented in the handoff, and the local issue was still created and updated.
  • +
+
+ +
+

Follow-up Work

+
    +
  • Run the app on a real iPhone and iPad, then update the validation checklist with concrete results.
  • +
  • Add a small diagnostics screen or log export if WebKit playback failures are hard to capture manually.
  • +
  • Only after hosted viability passes, pin and evaluate bundled stremio-web assets.
  • +
+
+
+ +