mirror of
https://github.com/dirtydishes/dreamio.git
synced 2026-06-06 21:38:15 +00:00
streamline native player controls
This commit is contained in:
parent
75e7a654a3
commit
c0a017ceb2
4 changed files with 290 additions and 33 deletions
|
|
@ -10,3 +10,5 @@
|
||||||
{"id":"int-27a61615","kind":"field_change","created_at":"2026-05-25T04:44:35.633997Z","actor":"dirtydishes","issue_id":"dreamio-ija","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed MobileVLCKit linker failures by preparing the XCFramework slice before app linking and preserving the integration through pod install."}}
|
{"id":"int-27a61615","kind":"field_change","created_at":"2026-05-25T04:44:35.633997Z","actor":"dirtydishes","issue_id":"dreamio-ija","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Fixed MobileVLCKit linker failures by preparing the XCFramework slice before app linking and preserving the integration through pod install."}}
|
||||||
{"id":"int-fad68cb4","kind":"field_change","created_at":"2026-05-25T05:04:55.103302Z","actor":"dirtydishes","issue_id":"dreamio-mj8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented native VLC player controls, caption controls, subtitle candidate discovery, and close-flow cleanup."}}
|
{"id":"int-fad68cb4","kind":"field_change","created_at":"2026-05-25T05:04:55.103302Z","actor":"dirtydishes","issue_id":"dreamio-mj8","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented native VLC player controls, caption controls, subtitle candidate discovery, and close-flow cleanup."}}
|
||||||
{"id":"int-6b806f87","kind":"field_change","created_at":"2026-05-25T09:49:39.908604Z","actor":"dirtydishes","issue_id":"dreamio-poo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and validated native player controls, subtitle handling refinements, and close-flow cleanup."}}
|
{"id":"int-6b806f87","kind":"field_change","created_at":"2026-05-25T09:49:39.908604Z","actor":"dirtydishes","issue_id":"dreamio-poo","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Implemented and validated native player controls, subtitle handling refinements, and close-flow cleanup."}}
|
||||||
|
{"id":"int-5d355e9b","kind":"field_change","created_at":"2026-05-25T09:51:17.04306Z","actor":"dirtydishes","issue_id":"dreamio-wgk","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Closed"}}
|
||||||
|
{"id":"int-9ddb7b1a","kind":"field_change","created_at":"2026-05-25T10:18:30.826897Z","actor":"dirtydishes","issue_id":"dreamio-7w6","extra":{"field":"status","new_value":"closed","old_value":"in_progress","reason":"Streamlined native player controls into a compact bottom overlay and validated the simulator build."}}
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@
|
||||||
{"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-l68","title":"Add native playback for direct debrid streams","description":"Implement a WKWebView JavaScript bridge that detects direct-file debrid media URLs and routes unsupported containers to a native player backend, initially MobileVLCKit, while preserving normal Stremio Web playback for compatible streams.","status":"closed","priority":1,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T03:13:19Z","created_by":"dirtydishes","updated_at":"2026-05-25T03:20:17Z","started_at":"2026-05-25T03:13:28Z","closed_at":"2026-05-25T03:20:17Z","close_reason":"Implemented native direct-stream bridge, classification, MobileVLCKit backend wiring, CocoaPods workflow docs, and turn documentation. Full iOS build is blocked locally by missing CocoaPods and iPhoneOS SDK.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-tnv","title":"Fix iOS bundle identifier install failure","description":"Xcode built Dreamio.app without a valid CFBundleIdentifier, causing device install to fail with CoreDeviceError 3000/3002. Investigate project bundle settings, fix the source configuration, validate the app bundle Info.plist, and document the change.","status":"closed","priority":1,"issue_type":"bug","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:23:00Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:25:36Z","started_at":"2026-05-25T01:23:07Z","closed_at":"2026-05-25T01:25:36Z","close_reason":"Added bundle metadata to Info.plist and validated processed app bundle identifier.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-4yn","title":"Build WKWebView MVP shell","description":"Create the first Dreamio MVP implementation: a minimal iOS WKWebView wrapper around hosted Stremio Web, with configuration, launch behavior, diagnostics, and documentation for real-device viability testing.","acceptance_criteria":"App project exists; WKWebView loads hosted Stremio Web; external/new-window navigation is handled; basic diagnostics and manual test documentation exist; quality gates are run or documented.","status":"closed","priority":1,"issue_type":"feature","owner":"dishes@dpdrm.com","created_at":"2026-05-24T14:55:12Z","created_by":"dirtydishes","updated_at":"2026-05-24T14:59:44Z","closed_at":"2026-05-24T14:59:44Z","close_reason":"Implemented the MVP WKWebView iOS shell, added run and validation documentation, and recorded current validation limits.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
{"_type":"issue","id":"dreamio-7w6","title":"Streamline native player controls","description":"Make the native playback controls take up less screen space while preserving play, seek, jump, captions, and close actions.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T10:15:49Z","created_by":"dirtydishes","updated_at":"2026-05-25T10:18:31Z","started_at":"2026-05-25T10:15:59Z","closed_at":"2026-05-25T10:18:31Z","close_reason":"Streamlined native player controls into a compact bottom overlay and validated the simulator build.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-mj8","title":"Add native player controls and captions","description":"Implement a fuller VLC-backed native playback surface with transport controls, caption controls, external subtitle discovery, and a clean close flow back to Stremio episode selection.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T04:57:53Z","created_by":"dirtydishes","updated_at":"2026-05-25T05:04:55Z","started_at":"2026-05-25T04:57:57Z","closed_at":"2026-05-25T05:04:55Z","close_reason":"Implemented native VLC player controls, caption controls, subtitle candidate discovery, and close-flow cleanup.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-mj8","title":"Add native player controls and captions","description":"Implement a fuller VLC-backed native playback surface with transport controls, caption controls, external subtitle discovery, and a clean close flow back to Stremio episode selection.","status":"closed","priority":2,"issue_type":"feature","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T04:57:53Z","created_by":"dirtydishes","updated_at":"2026-05-25T05:04:55Z","started_at":"2026-05-25T04:57:57Z","closed_at":"2026-05-25T05:04:55Z","close_reason":"Implemented native VLC player controls, caption controls, subtitle candidate discovery, and close-flow cleanup.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-evt","title":"Enable WebView inspection and playback diagnostics","description":"Add development-only WKWebView inspection and token-safe playback diagnostics so Dreamio can debug hosted Stremio media failures without changing app navigation, login, or playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T02:30:26Z","created_by":"dirtydishes","updated_at":"2026-05-25T02:34:55Z","started_at":"2026-05-25T02:30:32Z","closed_at":"2026-05-25T02:34:55Z","close_reason":"Implemented debug-only WKWebView inspection, token-safe playback diagnostics, navigation logging, validation build, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-evt","title":"Enable WebView inspection and playback diagnostics","description":"Add development-only WKWebView inspection and token-safe playback diagnostics so Dreamio can debug hosted Stremio media failures without changing app navigation, login, or playback behavior.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T02:30:26Z","created_by":"dirtydishes","updated_at":"2026-05-25T02:34:55Z","started_at":"2026-05-25T02:30:32Z","closed_at":"2026-05-25T02:34:55Z","close_reason":"Implemented debug-only WKWebView inspection, token-safe playback diagnostics, navigation logging, validation build, and turn documentation.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
{"_type":"issue","id":"dreamio-a5b","title":"Track HTML diff rendering tooling as dev dependency","description":"Move the HTML diff rendering package into devDependencies and ignore installed Node modules so the repo tracks reproducible tooling without vendoring dependencies.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:12:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:12:44Z","started_at":"2026-05-25T01:12:14Z","closed_at":"2026-05-25T01:12:44Z","close_reason":"Moved @pierre/diffs to devDependencies and ignored node_modules.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
{"_type":"issue","id":"dreamio-a5b","title":"Track HTML diff rendering tooling as dev dependency","description":"Move the HTML diff rendering package into devDependencies and ignore installed Node modules so the repo tracks reproducible tooling without vendoring dependencies.","status":"closed","priority":2,"issue_type":"task","assignee":"dirtydishes","owner":"dishes@dpdrm.com","created_at":"2026-05-25T01:12:07Z","created_by":"dirtydishes","updated_at":"2026-05-25T01:12:44Z","started_at":"2026-05-25T01:12:14Z","closed_at":"2026-05-25T01:12:44Z","close_reason":"Moved @pierre/diffs to devDependencies and ignored node_modules.","dependency_count":0,"dependent_count":0,"comment_count":0}
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ final class NativePlayerViewController: UIViewController {
|
||||||
button.setImage(UIImage(systemName: "xmark"), for: .normal)
|
button.setImage(UIImage(systemName: "xmark"), for: .normal)
|
||||||
button.tintColor = .white
|
button.tintColor = .white
|
||||||
button.backgroundColor = UIColor.black.withAlphaComponent(0.45)
|
button.backgroundColor = UIColor.black.withAlphaComponent(0.45)
|
||||||
button.layer.cornerRadius = 22
|
button.layer.cornerRadius = 18
|
||||||
button.accessibilityLabel = "Close"
|
button.accessibilityLabel = "Close"
|
||||||
return button
|
return button
|
||||||
}()
|
}()
|
||||||
|
|
@ -31,7 +31,7 @@ final class NativePlayerViewController: UIViewController {
|
||||||
private let controlsContainer: UIVisualEffectView = {
|
private let controlsContainer: UIVisualEffectView = {
|
||||||
let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark))
|
let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemUltraThinMaterialDark))
|
||||||
view.translatesAutoresizingMaskIntoConstraints = false
|
view.translatesAutoresizingMaskIntoConstraints = false
|
||||||
view.layer.cornerRadius = 12
|
view.layer.cornerRadius = 16
|
||||||
view.clipsToBounds = true
|
view.clipsToBounds = true
|
||||||
return view
|
return view
|
||||||
}()
|
}()
|
||||||
|
|
@ -52,7 +52,7 @@ final class NativePlayerViewController: UIViewController {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.textColor = .white
|
label.textColor = .white
|
||||||
label.font = .monospacedDigitSystemFont(ofSize: 13, weight: .medium)
|
label.font = .monospacedDigitSystemFont(ofSize: 11, weight: .semibold)
|
||||||
label.text = "0:00"
|
label.text = "0:00"
|
||||||
return label
|
return label
|
||||||
}()
|
}()
|
||||||
|
|
@ -61,7 +61,7 @@ final class NativePlayerViewController: UIViewController {
|
||||||
let label = UILabel()
|
let label = UILabel()
|
||||||
label.translatesAutoresizingMaskIntoConstraints = false
|
label.translatesAutoresizingMaskIntoConstraints = false
|
||||||
label.textColor = .white
|
label.textColor = .white
|
||||||
label.font = .monospacedDigitSystemFont(ofSize: 13, weight: .medium)
|
label.font = .monospacedDigitSystemFont(ofSize: 11, weight: .semibold)
|
||||||
label.textAlignment = .right
|
label.textAlignment = .right
|
||||||
label.text = "-0:00"
|
label.text = "-0:00"
|
||||||
return label
|
return label
|
||||||
|
|
@ -75,6 +75,8 @@ final class NativePlayerViewController: UIViewController {
|
||||||
slider.minimumTrackTintColor = UIColor(red: 0.64, green: 0.48, blue: 1.0, alpha: 1)
|
slider.minimumTrackTintColor = UIColor(red: 0.64, green: 0.48, blue: 1.0, alpha: 1)
|
||||||
slider.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3)
|
slider.maximumTrackTintColor = UIColor.white.withAlphaComponent(0.3)
|
||||||
slider.thumbTintColor = .white
|
slider.thumbTintColor = .white
|
||||||
|
slider.setThumbImage(NativePlayerViewController.scrubberThumbImage(diameter: 12), for: .normal)
|
||||||
|
slider.setThumbImage(NativePlayerViewController.scrubberThumbImage(diameter: 16), for: .highlighted)
|
||||||
return slider
|
return slider
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
|
@ -184,6 +186,7 @@ final class NativePlayerViewController: UIViewController {
|
||||||
jumpBackButton.addTarget(self, action: #selector(jumpBack), for: .touchUpInside)
|
jumpBackButton.addTarget(self, action: #selector(jumpBack), for: .touchUpInside)
|
||||||
jumpForwardButton.addTarget(self, action: #selector(jumpForward), for: .touchUpInside)
|
jumpForwardButton.addTarget(self, action: #selector(jumpForward), for: .touchUpInside)
|
||||||
captionsButton.addTarget(self, action: #selector(showCaptions), for: .touchUpInside)
|
captionsButton.addTarget(self, action: #selector(showCaptions), for: .touchUpInside)
|
||||||
|
playPauseButton.layer.cornerRadius = 21
|
||||||
scrubber.addTarget(self, action: #selector(scrubbingStarted), for: .touchDown)
|
scrubber.addTarget(self, action: #selector(scrubbingStarted), for: .touchDown)
|
||||||
scrubber.addTarget(self, action: #selector(scrubberChanged), for: .valueChanged)
|
scrubber.addTarget(self, action: #selector(scrubberChanged), for: .valueChanged)
|
||||||
scrubber.addTarget(self, action: #selector(scrubbingEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
|
scrubber.addTarget(self, action: #selector(scrubbingEnded), for: [.touchUpInside, .touchUpOutside, .touchCancel])
|
||||||
|
|
@ -192,22 +195,23 @@ final class NativePlayerViewController: UIViewController {
|
||||||
tap.cancelsTouchesInView = false
|
tap.cancelsTouchesInView = false
|
||||||
tapSurfaceView.addGestureRecognizer(tap)
|
tapSurfaceView.addGestureRecognizer(tap)
|
||||||
|
|
||||||
|
let timeAndScrubRow = UIStackView(arrangedSubviews: [elapsedLabel, scrubber, remainingLabel])
|
||||||
|
timeAndScrubRow.translatesAutoresizingMaskIntoConstraints = false
|
||||||
|
timeAndScrubRow.axis = .horizontal
|
||||||
|
timeAndScrubRow.alignment = .center
|
||||||
|
timeAndScrubRow.spacing = 8
|
||||||
|
|
||||||
let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
|
let controlRow = UIStackView(arrangedSubviews: [jumpBackButton, playPauseButton, jumpForwardButton, captionsButton])
|
||||||
controlRow.translatesAutoresizingMaskIntoConstraints = false
|
controlRow.translatesAutoresizingMaskIntoConstraints = false
|
||||||
controlRow.axis = .horizontal
|
controlRow.axis = .horizontal
|
||||||
controlRow.alignment = .center
|
controlRow.alignment = .center
|
||||||
controlRow.distribution = .equalCentering
|
controlRow.distribution = .equalSpacing
|
||||||
controlRow.spacing = 18
|
controlRow.spacing = 14
|
||||||
|
|
||||||
let timeRow = UIStackView(arrangedSubviews: [elapsedLabel, remainingLabel])
|
let stack = UIStackView(arrangedSubviews: [timeAndScrubRow, controlRow])
|
||||||
timeRow.translatesAutoresizingMaskIntoConstraints = false
|
|
||||||
timeRow.axis = .horizontal
|
|
||||||
timeRow.distribution = .fillEqually
|
|
||||||
|
|
||||||
let stack = UIStackView(arrangedSubviews: [scrubber, timeRow, controlRow])
|
|
||||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||||
stack.axis = .vertical
|
stack.axis = .vertical
|
||||||
stack.spacing = 8
|
stack.spacing = 6
|
||||||
controlsContainer.contentView.addSubview(stack)
|
controlsContainer.contentView.addSubview(stack)
|
||||||
|
|
||||||
NSLayoutConstraint.activate([
|
NSLayoutConstraint.activate([
|
||||||
|
|
@ -228,28 +232,33 @@ final class NativePlayerViewController: UIViewController {
|
||||||
failureLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),
|
failureLabel.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -28),
|
||||||
failureLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
failureLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor),
|
||||||
|
|
||||||
closeButton.widthAnchor.constraint(equalToConstant: 44),
|
closeButton.widthAnchor.constraint(equalToConstant: 36),
|
||||||
closeButton.heightAnchor.constraint(equalToConstant: 44),
|
closeButton.heightAnchor.constraint(equalToConstant: 36),
|
||||||
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 12),
|
closeButton.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor, constant: 10),
|
||||||
closeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -12),
|
closeButton.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -10),
|
||||||
|
|
||||||
controlsContainer.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor, constant: 18),
|
controlsContainer.centerXAnchor.constraint(equalTo: view.safeAreaLayoutGuide.centerXAnchor),
|
||||||
controlsContainer.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor, constant: -18),
|
controlsContainer.widthAnchor.constraint(lessThanOrEqualTo: view.safeAreaLayoutGuide.widthAnchor, constant: -24),
|
||||||
controlsContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -18),
|
controlsContainer.widthAnchor.constraint(lessThanOrEqualToConstant: 430),
|
||||||
|
controlsContainer.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor, constant: -12),
|
||||||
|
|
||||||
stack.leadingAnchor.constraint(equalTo: controlsContainer.contentView.leadingAnchor, constant: 16),
|
stack.leadingAnchor.constraint(equalTo: controlsContainer.contentView.leadingAnchor, constant: 12),
|
||||||
stack.trailingAnchor.constraint(equalTo: controlsContainer.contentView.trailingAnchor, constant: -16),
|
stack.trailingAnchor.constraint(equalTo: controlsContainer.contentView.trailingAnchor, constant: -12),
|
||||||
stack.topAnchor.constraint(equalTo: controlsContainer.contentView.topAnchor, constant: 14),
|
stack.topAnchor.constraint(equalTo: controlsContainer.contentView.topAnchor, constant: 8),
|
||||||
stack.bottomAnchor.constraint(equalTo: controlsContainer.contentView.bottomAnchor, constant: -14),
|
stack.bottomAnchor.constraint(equalTo: controlsContainer.contentView.bottomAnchor, constant: -10),
|
||||||
|
|
||||||
jumpBackButton.widthAnchor.constraint(equalToConstant: 44),
|
elapsedLabel.widthAnchor.constraint(equalToConstant: 42),
|
||||||
jumpBackButton.heightAnchor.constraint(equalToConstant: 44),
|
remainingLabel.widthAnchor.constraint(equalToConstant: 42),
|
||||||
playPauseButton.widthAnchor.constraint(equalToConstant: 54),
|
scrubber.widthAnchor.constraint(greaterThanOrEqualToConstant: 160),
|
||||||
playPauseButton.heightAnchor.constraint(equalToConstant: 54),
|
|
||||||
jumpForwardButton.widthAnchor.constraint(equalToConstant: 44),
|
jumpBackButton.widthAnchor.constraint(equalToConstant: 36),
|
||||||
jumpForwardButton.heightAnchor.constraint(equalToConstant: 44),
|
jumpBackButton.heightAnchor.constraint(equalToConstant: 36),
|
||||||
captionsButton.widthAnchor.constraint(equalToConstant: 44),
|
playPauseButton.widthAnchor.constraint(equalToConstant: 42),
|
||||||
captionsButton.heightAnchor.constraint(equalToConstant: 44)
|
playPauseButton.heightAnchor.constraint(equalToConstant: 42),
|
||||||
|
jumpForwardButton.widthAnchor.constraint(equalToConstant: 36),
|
||||||
|
jumpForwardButton.heightAnchor.constraint(equalToConstant: 36),
|
||||||
|
captionsButton.widthAnchor.constraint(equalToConstant: 36),
|
||||||
|
captionsButton.heightAnchor.constraint(equalToConstant: 36)
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -385,8 +394,17 @@ final class NativePlayerViewController: UIViewController {
|
||||||
button.setImage(UIImage(systemName: systemName), for: .normal)
|
button.setImage(UIImage(systemName: systemName), for: .normal)
|
||||||
button.tintColor = .white
|
button.tintColor = .white
|
||||||
button.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
button.backgroundColor = UIColor.black.withAlphaComponent(0.35)
|
||||||
button.layer.cornerRadius = 22
|
button.layer.cornerRadius = 18
|
||||||
button.accessibilityLabel = label
|
button.accessibilityLabel = label
|
||||||
return button
|
return button
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static func scrubberThumbImage(diameter: CGFloat) -> UIImage {
|
||||||
|
let format = UIGraphicsImageRendererFormat()
|
||||||
|
format.scale = UIScreen.main.scale
|
||||||
|
return UIGraphicsImageRenderer(size: CGSize(width: diameter, height: diameter), format: format).image { context in
|
||||||
|
UIColor.white.setFill()
|
||||||
|
context.cgContext.fillEllipse(in: CGRect(origin: .zero, size: CGSize(width: diameter, height: diameter)))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
236
docs/turns/2026-05-25-streamline-native-player-controls.html
Normal file
236
docs/turns/2026-05-25-streamline-native-player-controls.html
Normal file
File diff suppressed because one or more lines are too long
Loading…
Add table
Add a link
Reference in a new issue