build wkwebview mvp shell

This commit is contained in:
dirtydishes 2026-05-24 10:59:57 -04:00
parent e8993ee7d1
commit d4e49cde1e
7 changed files with 830 additions and 0 deletions

View file

@ -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 = "<group>"; };
6F2A2B342C00100100DREAMIO /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = "<group>"; };
6F2A2B352C00100100DREAMIO /* DreamioWebViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DreamioWebViewController.swift; sourceTree = "<group>"; };
6F2A2B392C00100100DREAMIO /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
/* 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 = "<group>";
};
6F2A2B312C00100100DREAMIO /* Products */ = {
isa = PBXGroup;
children = (
6F2A2B302C00100100DREAMIO /* Dreamio.app */,
);
name = Products;
sourceTree = "<group>";
};
6F2A2B322C00100100DREAMIO /* Dreamio */ = {
isa = PBXGroup;
children = (
6F2A2B332C00100100DREAMIO /* AppDelegate.swift */,
6F2A2B342C00100100DREAMIO /* SceneDelegate.swift */,
6F2A2B352C00100100DREAMIO /* DreamioWebViewController.swift */,
6F2A2B392C00100100DREAMIO /* Info.plist */,
);
path = Dreamio;
sourceTree = "<group>";
};
/* 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 */;
}

19
Dreamio/AppDelegate.swift Normal file
View file

@ -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)
}
}

View file

@ -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
}
}

38
Dreamio/Info.plist Normal file
View file

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>UIApplicationSceneManifest</key>
<dict>
<key>UIApplicationSupportsMultipleScenes</key>
<false/>
<key>UISceneConfigurations</key>
<dict>
<key>UIWindowSceneSessionRoleApplication</key>
<array>
<dict>
<key>UISceneConfigurationName</key>
<string>Default Configuration</string>
<key>UISceneDelegateClassName</key>
<string>$(PRODUCT_MODULE_NAME).SceneDelegate</string>
</dict>
</array>
</dict>
</dict>
<key>UILaunchScreen</key>
<dict/>
<key>UISupportedInterfaceOrientations</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
<key>UISupportedInterfaceOrientations~ipad</key>
<array>
<string>UIInterfaceOrientationPortrait</string>
<string>UIInterfaceOrientationPortraitUpsideDown</string>
<string>UIInterfaceOrientationLandscapeLeft</string>
<string>UIInterfaceOrientationLandscapeRight</string>
</array>
</dict>
</plist>

View file

@ -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
}
}

34
README.md Normal file
View file

@ -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.

View file

@ -0,0 +1,260 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Dreamio WKWebView MVP Shell</title>
<style>
:root {
color-scheme: light;
--ink: #182033;
--muted: #5e6678;
--paper: #f7f4ee;
--panel: #fffdfa;
--line: #ded7ca;
--accent: #6f4fd8;
--accent-soft: #ede7ff;
--code: #282336;
}
body {
margin: 0;
background: var(--paper);
color: var(--ink);
font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
line-height: 1.6;
}
main {
width: min(920px, calc(100% - 40px));
margin: 0 auto;
padding: 56px 0 72px;
}
header {
margin-bottom: 40px;
}
h1 {
max-width: 760px;
margin: 0 0 12px;
font-size: clamp(2rem, 5vw, 4rem);
line-height: 1;
letter-spacing: 0;
}
h2 {
margin: 40px 0 12px;
font-size: 1.25rem;
line-height: 1.2;
}
p {
max-width: 72ch;
margin: 0 0 14px;
}
ul {
max-width: 76ch;
padding-left: 1.25rem;
}
li {
margin: 0.35rem 0;
}
code,
pre {
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
}
code {
color: var(--code);
background: var(--accent-soft);
padding: 0.08rem 0.28rem;
border-radius: 4px;
}
pre {
overflow-x: auto;
padding: 18px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.summary {
max-width: 760px;
padding: 22px;
border: 1px solid var(--line);
border-radius: 8px;
background: var(--panel);
}
.eyebrow {
margin-bottom: 10px;
color: var(--accent);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
font-size: 0.78rem;
}
.diff-fallback {
display: block;
white-space: pre;
}
</style>
<script type="module">
import { FileDiff } from "https://esm.sh/@pierre/diffs";
const snippets = [
{
id: "webview-diff",
oldFile: { name: "DreamioWebViewController.swift", contents: "" },
newFile: {
name: "DreamioWebViewController.swift",
contents: `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
return WKWebView(frame: .zero, configuration: configuration)
}()
}`
}
},
{
id: "readme-diff",
oldFile: { name: "README.md", contents: "" },
newFile: {
name: "README.md",
contents: `# Dreamio
Dreamio is a minimal iOS WKWebView wrapper around hosted Stremio Web.
## MVP Validation Checklist
- Cold launch loads hosted Stremio Web.
- Login completes and persists after app relaunch.
- HLS direct stream playback works.
- MP4 direct stream playback works.`
}
}
];
for (const snippet of snippets) {
const target = document.getElementById(snippet.id);
if (!target) continue;
try {
new FileDiff({ theme: "github-light" }).render({
oldFile: snippet.oldFile,
newFile: snippet.newFile,
containerWrapper: target
});
} catch {
target.hidden = true;
}
}
</script>
</head>
<body>
<main>
<header>
<div class="eyebrow">Repository implementation turn</div>
<h1>Dreamio WKWebView MVP shell</h1>
<p class="summary">Created the first runnable Dreamio app shape: a thin iOS UIKit host with a single <code>WKWebView</code> 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.</p>
</header>
<section>
<h2>Summary</h2>
<p>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 <code>WKWebView</code> on real iPhone and iPad hardware.</p>
</section>
<section>
<h2>Changes Made</h2>
<ul>
<li>Added <code>Dreamio.xcodeproj</code> with a single iOS application target.</li>
<li>Added a UIKit lifecycle with <code>AppDelegate</code> and <code>SceneDelegate</code>.</li>
<li>Added <code>DreamioWebViewController</code>, which loads <code>https://web.stremio.com/</code> in a configured <code>WKWebView</code>.</li>
<li>Enabled inline media playback, JavaScript window opening, back-forward gestures, and a small load progress indicator.</li>
<li>Added basic external URL handling and in-place handling for new-window flows.</li>
<li>Added <code>README.md</code> with run instructions and the MVP validation checklist.</li>
</ul>
</section>
<section>
<h2>Context</h2>
<p>The source plan in <code>/Users/kell/dreamio-plan.md</code> 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.</p>
</section>
<section>
<h2>Important Implementation Details</h2>
<ul>
<li>The MVP URL is centralized in <code>DreamioWebViewController.Constants.stremioWebURL</code>.</li>
<li><code>WKUIDelegate</code> loads targetless popup requests in the existing web view so auth or addon flows do not vanish.</li>
<li>Non-HTTP user-activated links such as <code>mailto:</code>, <code>tel:</code>, and <code>sms:</code> open externally.</li>
<li>Orientation support includes portrait and landscape on iPhone, plus upside-down portrait on iPad.</li>
<li>Code signing is left automatic with an empty development team, so Xcode can prompt for the correct personal or team signing identity.</li>
</ul>
</section>
<section>
<h2>Relevant Diff Snippets</h2>
<p>The snippets below are rendered with <a href="https://diffs.com/docs">Diffs</a> when the document has network access, with plain-code fallback content retained in the page.</p>
<div id="webview-diff"></div>
<pre class="diff-fallback"><code>+++ 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.</code></pre>
<div id="readme-diff"></div>
<pre class="diff-fallback"><code>+++ README.md
+ Added Xcode run instructions.
+ Added hosted web, login, addon, HLS, MP4, fullscreen, rotation, and relaunch validation checklist.</code></pre>
</section>
<section>
<h2>Expected Impact for End-Users</h2>
<p>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.</p>
</section>
<section>
<h2>Validation</h2>
<ul>
<li>Ran <code>plutil -lint Dreamio/Info.plist</code>: passed.</li>
<li>Ran <code>plutil -lint Dreamio.xcodeproj/project.pbxproj</code>: passed.</li>
<li>Checked local Swift availability with <code>swift --version</code>: available.</li>
<li>Attempted to check Xcode build availability with <code>xcodebuild -version</code>: blocked because the active developer directory is Command Line Tools, not full Xcode.</li>
</ul>
</section>
<section>
<h2>Issues, Limitations, and Mitigations</h2>
<ul>
<li>The app has not been compiled on this machine because full Xcode is not selected. Mitigation: open the project in Xcode or switch <code>xcode-select</code> to a full Xcode install, then build on device.</li>
<li>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.</li>
<li>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.</li>
</ul>
</section>
<section>
<h2>Follow-up Work</h2>
<ul>
<li>Run the app on a real iPhone and iPad, then update the validation checklist with concrete results.</li>
<li>Add a small diagnostics screen or log export if WebKit playback failures are hard to capture manually.</li>
<li>Only after hosted viability passes, pin and evaluate bundled <code>stremio-web</code> assets.</li>
</ul>
</section>
</main>
</body>
</html>