feat: revamp iOS share extension UX and improve Prime URL parsing
- redesign Share Extension with dark streaming-inspired layout - add dynamic half-star rating interaction with stronger haptics - improve dismiss behavior and comments composer UX - support app.primevideo.com share links via gti parsing
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
import UIKit
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
final class ShareViewController: UIViewController {
|
||||
|
||||
// MARK: – View state
|
||||
final class ShareViewController: UIViewController, UITextViewDelegate {
|
||||
|
||||
private enum ViewState {
|
||||
case loading
|
||||
@@ -11,59 +9,97 @@ final class ShareViewController: UIViewController {
|
||||
case error(String)
|
||||
}
|
||||
|
||||
// MARK: – Header
|
||||
private struct CommentItem {
|
||||
let user: String
|
||||
let body: String
|
||||
let time: String
|
||||
}
|
||||
|
||||
private let headerView = UIView()
|
||||
private let headerLabel = UILabel()
|
||||
private let closeButton = UIButton(type: .system)
|
||||
private var selectedRating = 0.0
|
||||
private var starButtons: [UIButton] = []
|
||||
private var comments: [CommentItem] = []
|
||||
|
||||
// MARK: – Loading / error
|
||||
private let headerView = UIView()
|
||||
private let headerLabel = UILabel()
|
||||
private let closeButton = UIButton(type: .system)
|
||||
|
||||
private let spinner = UIActivityIndicatorView(style: .large)
|
||||
private let messageLabel = UILabel()
|
||||
private let overlayView = UIView()
|
||||
private let spinner = UIActivityIndicatorView(style: .large)
|
||||
private let overlayLabel = UILabel()
|
||||
|
||||
// MARK: – Metadata card (genişletilebilir — buraya puan/yorum ekleyebilirsin)
|
||||
private let scrollView = UIScrollView()
|
||||
private let contentStack = UIStackView()
|
||||
|
||||
private let scrollView = UIScrollView()
|
||||
/// Bu stack'e ileride yıldız seçici, yorum alanı vb. ekleyebilirsin.
|
||||
let contentStack = UIStackView()
|
||||
private let backdropContainer = UIView()
|
||||
private let backdropImageView = UIImageView()
|
||||
private let gradientLayer = CAGradientLayer()
|
||||
private let providerBadge = UILabel()
|
||||
private let heroTitleLabel = UILabel()
|
||||
private let heroMetaLabel = UILabel()
|
||||
|
||||
private let providerLabel = UILabel()
|
||||
private let titleLabel = UILabel()
|
||||
private let metaLabel = UILabel()
|
||||
private let genresLabel = UILabel()
|
||||
private let plotLabel = UILabel()
|
||||
private let genreScroll = UIScrollView()
|
||||
private let genreStack = UIStackView()
|
||||
private let plotLabel = UILabel()
|
||||
private let castLabel = UILabel()
|
||||
|
||||
// MARK: – Lifecycle
|
||||
private let commentsListStack = UIStackView()
|
||||
private let commentTextView = UITextView()
|
||||
private let commentPlaceholderLabel = UILabel()
|
||||
private let submitCommentButton = UIButton(type: .system)
|
||||
private let starsRow = UIStackView()
|
||||
|
||||
private let hoverHaptic = UIImpactFeedbackGenerator(style: .light)
|
||||
private var dismissPanStartTransform: CGAffineTransform = .identity
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
view.backgroundColor = .systemBackground
|
||||
overrideUserInterfaceStyle = .dark
|
||||
// Prevent system pull-down dismiss that only closes this extension UI.
|
||||
// We'll handle downward dismiss ourselves and always call completeRequest.
|
||||
isModalInPresentation = true
|
||||
view.backgroundColor = UIColor(red: 0.04, green: 0.04, blue: 0.06, alpha: 1)
|
||||
|
||||
setupHeader()
|
||||
setupLoadingViews()
|
||||
setupScrollView()
|
||||
setupOverlay()
|
||||
setupFeedback()
|
||||
|
||||
Task { await handleIncomingShare() }
|
||||
}
|
||||
|
||||
// MARK: – Setup
|
||||
override func viewDidLayoutSubviews() {
|
||||
super.viewDidLayoutSubviews()
|
||||
gradientLayer.frame = backdropContainer.bounds
|
||||
}
|
||||
|
||||
private func setupFeedback() {
|
||||
hoverHaptic.prepare()
|
||||
}
|
||||
|
||||
private func setupHeader() {
|
||||
headerView.backgroundColor = .systemBackground
|
||||
headerView.backgroundColor = UIColor(red: 0.06, green: 0.06, blue: 0.08, alpha: 0.96)
|
||||
headerView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(headerView)
|
||||
|
||||
headerLabel.text = "Ratebubble"
|
||||
headerLabel.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
headerLabel.textColor = .white
|
||||
headerLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
headerView.addSubview(headerLabel)
|
||||
|
||||
closeButton.setTitle("Kapat", for: .normal)
|
||||
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
||||
closeButton.setTitleColor(.systemGray2, for: .normal)
|
||||
closeButton.titleLabel?.font = .systemFont(ofSize: 16, weight: .medium)
|
||||
closeButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside)
|
||||
headerView.addSubview(closeButton)
|
||||
|
||||
let dismissPan = UIPanGestureRecognizer(target: self, action: #selector(handleDismissPan(_:)))
|
||||
dismissPan.maximumNumberOfTouches = 1
|
||||
headerView.addGestureRecognizer(dismissPan)
|
||||
|
||||
let separator = UIView()
|
||||
separator.backgroundColor = .separator
|
||||
separator.backgroundColor = UIColor.white.withAlphaComponent(0.08)
|
||||
separator.translatesAutoresizingMaskIntoConstraints = false
|
||||
headerView.addSubview(separator)
|
||||
|
||||
@@ -71,7 +107,7 @@ final class ShareViewController: UIViewController {
|
||||
headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
|
||||
headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
headerView.heightAnchor.constraint(equalToConstant: 44),
|
||||
headerView.heightAnchor.constraint(equalToConstant: 46),
|
||||
|
||||
headerLabel.centerXAnchor.constraint(equalTo: headerView.centerXAnchor),
|
||||
headerLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor),
|
||||
@@ -82,156 +118,592 @@ final class ShareViewController: UIViewController {
|
||||
separator.leadingAnchor.constraint(equalTo: headerView.leadingAnchor),
|
||||
separator.trailingAnchor.constraint(equalTo: headerView.trailingAnchor),
|
||||
separator.bottomAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5),
|
||||
separator.heightAnchor.constraint(equalToConstant: 0.5)
|
||||
])
|
||||
}
|
||||
|
||||
private func setupLoadingViews() {
|
||||
spinner.translatesAutoresizingMaskIntoConstraints = false
|
||||
spinner.hidesWhenStopped = true
|
||||
view.addSubview(spinner)
|
||||
|
||||
messageLabel.text = "Analiz ediliyor..."
|
||||
messageLabel.textColor = .secondaryLabel
|
||||
messageLabel.textAlignment = .center
|
||||
messageLabel.numberOfLines = 0
|
||||
messageLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(messageLabel)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
spinner.centerXAnchor.constraint(equalTo: view.centerXAnchor),
|
||||
spinner.centerYAnchor.constraint(equalTo: view.centerYAnchor, constant: -16),
|
||||
messageLabel.topAnchor.constraint(equalTo: spinner.bottomAnchor, constant: 12),
|
||||
messageLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 24),
|
||||
messageLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -24),
|
||||
])
|
||||
|
||||
spinner.startAnimating()
|
||||
}
|
||||
|
||||
private func setupScrollView() {
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.alwaysBounceVertical = true
|
||||
scrollView.keyboardDismissMode = .interactive
|
||||
scrollView.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.isHidden = true
|
||||
view.addSubview(scrollView)
|
||||
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 8
|
||||
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(contentStack)
|
||||
|
||||
// Provider (NETFLIX / PRIME VIDEO)
|
||||
providerLabel.font = .systemFont(ofSize: 11, weight: .bold)
|
||||
|
||||
// Title
|
||||
titleLabel.font = .systemFont(ofSize: 26, weight: .bold)
|
||||
titleLabel.numberOfLines = 3
|
||||
|
||||
// Year · Type · Season
|
||||
metaLabel.font = .systemFont(ofSize: 14)
|
||||
metaLabel.textColor = .secondaryLabel
|
||||
|
||||
// Genres
|
||||
genresLabel.font = .systemFont(ofSize: 13)
|
||||
genresLabel.textColor = .secondaryLabel
|
||||
genresLabel.numberOfLines = 2
|
||||
|
||||
// Plot
|
||||
plotLabel.font = .systemFont(ofSize: 14)
|
||||
plotLabel.numberOfLines = 5
|
||||
plotLabel.textColor = .label
|
||||
|
||||
contentStack.addArrangedSubview(providerLabel)
|
||||
contentStack.addArrangedSubview(titleLabel)
|
||||
contentStack.setCustomSpacing(4, after: providerLabel)
|
||||
contentStack.addArrangedSubview(metaLabel)
|
||||
contentStack.addArrangedSubview(genresLabel)
|
||||
contentStack.setCustomSpacing(12, after: genresLabel)
|
||||
contentStack.addArrangedSubview(plotLabel)
|
||||
|
||||
// ── Buraya ileride yorum/puan UI'ı eklenecek ──
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
scrollView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
scrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
scrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
scrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor)
|
||||
])
|
||||
|
||||
contentStack.topAnchor.constraint(equalTo: scrollView.topAnchor, constant: 20),
|
||||
contentStack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 20),
|
||||
contentStack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -20),
|
||||
contentStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -40),
|
||||
contentStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -32),
|
||||
setupBackdrop()
|
||||
setupContentStack()
|
||||
setupMetadataSection()
|
||||
setupRatingSection()
|
||||
setupCommentsSection()
|
||||
}
|
||||
|
||||
private func setupBackdrop() {
|
||||
backdropContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropContainer.clipsToBounds = true
|
||||
scrollView.addSubview(backdropContainer)
|
||||
|
||||
backdropImageView.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropImageView.contentMode = .scaleAspectFill
|
||||
backdropImageView.backgroundColor = UIColor(red: 0.10, green: 0.10, blue: 0.12, alpha: 1)
|
||||
backdropImageView.alpha = 0
|
||||
backdropContainer.addSubview(backdropImageView)
|
||||
|
||||
gradientLayer.colors = [
|
||||
UIColor.clear.cgColor,
|
||||
UIColor.black.withAlphaComponent(0.40).cgColor,
|
||||
UIColor.black.withAlphaComponent(0.92).cgColor
|
||||
]
|
||||
gradientLayer.locations = [0.25, 0.55, 1.0]
|
||||
backdropContainer.layer.addSublayer(gradientLayer)
|
||||
|
||||
providerBadge.font = .systemFont(ofSize: 10, weight: .bold)
|
||||
providerBadge.textColor = .white
|
||||
providerBadge.layer.cornerRadius = 10
|
||||
providerBadge.layer.masksToBounds = true
|
||||
providerBadge.textAlignment = .center
|
||||
providerBadge.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropContainer.addSubview(providerBadge)
|
||||
|
||||
heroTitleLabel.font = .systemFont(ofSize: 28, weight: .heavy)
|
||||
heroTitleLabel.textColor = .white
|
||||
heroTitleLabel.numberOfLines = 2
|
||||
heroTitleLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropContainer.addSubview(heroTitleLabel)
|
||||
|
||||
heroMetaLabel.font = .systemFont(ofSize: 14, weight: .medium)
|
||||
heroMetaLabel.textColor = UIColor.white.withAlphaComponent(0.78)
|
||||
heroMetaLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
backdropContainer.addSubview(heroMetaLabel)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
backdropContainer.topAnchor.constraint(equalTo: scrollView.topAnchor),
|
||||
backdropContainer.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor),
|
||||
backdropContainer.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor),
|
||||
backdropContainer.widthAnchor.constraint(equalTo: scrollView.widthAnchor),
|
||||
backdropContainer.heightAnchor.constraint(equalToConstant: 248),
|
||||
|
||||
backdropImageView.topAnchor.constraint(equalTo: backdropContainer.topAnchor),
|
||||
backdropImageView.leadingAnchor.constraint(equalTo: backdropContainer.leadingAnchor),
|
||||
backdropImageView.trailingAnchor.constraint(equalTo: backdropContainer.trailingAnchor),
|
||||
backdropImageView.bottomAnchor.constraint(equalTo: backdropContainer.bottomAnchor),
|
||||
|
||||
providerBadge.leadingAnchor.constraint(equalTo: backdropContainer.leadingAnchor, constant: 16),
|
||||
providerBadge.bottomAnchor.constraint(equalTo: heroTitleLabel.topAnchor, constant: -10),
|
||||
providerBadge.heightAnchor.constraint(equalToConstant: 20),
|
||||
providerBadge.widthAnchor.constraint(greaterThanOrEqualToConstant: 78),
|
||||
|
||||
heroTitleLabel.leadingAnchor.constraint(equalTo: backdropContainer.leadingAnchor, constant: 16),
|
||||
heroTitleLabel.trailingAnchor.constraint(equalTo: backdropContainer.trailingAnchor, constant: -16),
|
||||
heroTitleLabel.bottomAnchor.constraint(equalTo: heroMetaLabel.topAnchor, constant: -5),
|
||||
|
||||
heroMetaLabel.leadingAnchor.constraint(equalTo: backdropContainer.leadingAnchor, constant: 16),
|
||||
heroMetaLabel.trailingAnchor.constraint(equalTo: backdropContainer.trailingAnchor, constant: -16),
|
||||
heroMetaLabel.bottomAnchor.constraint(equalTo: backdropContainer.bottomAnchor, constant: -16)
|
||||
])
|
||||
}
|
||||
|
||||
// MARK: – State application
|
||||
private func setupContentStack() {
|
||||
contentStack.axis = .vertical
|
||||
contentStack.spacing = 16
|
||||
contentStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
scrollView.addSubview(contentStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
contentStack.topAnchor.constraint(equalTo: backdropContainer.bottomAnchor, constant: 14),
|
||||
contentStack.leadingAnchor.constraint(equalTo: scrollView.leadingAnchor, constant: 14),
|
||||
contentStack.trailingAnchor.constraint(equalTo: scrollView.trailingAnchor, constant: -14),
|
||||
contentStack.widthAnchor.constraint(equalTo: scrollView.widthAnchor, constant: -28),
|
||||
contentStack.bottomAnchor.constraint(equalTo: scrollView.bottomAnchor, constant: -30)
|
||||
])
|
||||
}
|
||||
|
||||
private func setupMetadataSection() {
|
||||
let card = makeSectionCard()
|
||||
let stack = makeCardStack()
|
||||
|
||||
genreScroll.showsHorizontalScrollIndicator = false
|
||||
genreScroll.translatesAutoresizingMaskIntoConstraints = false
|
||||
genreScroll.heightAnchor.constraint(equalToConstant: 32).isActive = true
|
||||
|
||||
genreStack.axis = .horizontal
|
||||
genreStack.spacing = 8
|
||||
genreStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
genreScroll.addSubview(genreStack)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
genreStack.topAnchor.constraint(equalTo: genreScroll.contentLayoutGuide.topAnchor),
|
||||
genreStack.leadingAnchor.constraint(equalTo: genreScroll.contentLayoutGuide.leadingAnchor),
|
||||
genreStack.trailingAnchor.constraint(equalTo: genreScroll.contentLayoutGuide.trailingAnchor),
|
||||
genreStack.bottomAnchor.constraint(equalTo: genreScroll.contentLayoutGuide.bottomAnchor),
|
||||
genreStack.heightAnchor.constraint(equalTo: genreScroll.frameLayoutGuide.heightAnchor)
|
||||
])
|
||||
|
||||
plotLabel.font = .systemFont(ofSize: 14)
|
||||
plotLabel.textColor = UIColor.white.withAlphaComponent(0.84)
|
||||
plotLabel.numberOfLines = 6
|
||||
|
||||
castLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
castLabel.textColor = UIColor.white.withAlphaComponent(0.66)
|
||||
castLabel.numberOfLines = 2
|
||||
|
||||
stack.addArrangedSubview(genreScroll)
|
||||
stack.addArrangedSubview(plotLabel)
|
||||
stack.addArrangedSubview(castLabel)
|
||||
card.addSubview(stack)
|
||||
pinCardStack(stack, in: card)
|
||||
contentStack.addArrangedSubview(card)
|
||||
}
|
||||
|
||||
private func setupRatingSection() {
|
||||
let card = makeSectionCard()
|
||||
let stack = makeCardStack()
|
||||
|
||||
let title = UILabel()
|
||||
title.text = "Puanla"
|
||||
title.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
title.textColor = .white
|
||||
|
||||
let subtitle = UILabel()
|
||||
subtitle.text = "Puanını istediğin zaman değiştirebilirsin."
|
||||
subtitle.font = .systemFont(ofSize: 12, weight: .regular)
|
||||
subtitle.textColor = UIColor.white.withAlphaComponent(0.55)
|
||||
|
||||
starsRow.axis = .horizontal
|
||||
starsRow.alignment = .center
|
||||
starsRow.distribution = .fillEqually
|
||||
starsRow.spacing = 4
|
||||
starsRow.translatesAutoresizingMaskIntoConstraints = false
|
||||
starsRow.isUserInteractionEnabled = true
|
||||
starsRow.heightAnchor.constraint(equalToConstant: 48).isActive = true
|
||||
|
||||
let panGesture = UIPanGestureRecognizer(target: self, action: #selector(handleStarPan(_:)))
|
||||
panGesture.maximumNumberOfTouches = 1
|
||||
starsRow.addGestureRecognizer(panGesture)
|
||||
|
||||
let symbolConfig = UIImage.SymbolConfiguration(pointSize: 30, weight: .medium)
|
||||
for i in 1...5 {
|
||||
let button = UIButton(type: .system)
|
||||
button.setImage(UIImage(systemName: "star", withConfiguration: symbolConfig), for: .normal)
|
||||
button.tintColor = UIColor.white.withAlphaComponent(0.18)
|
||||
button.tag = i
|
||||
button.addTarget(self, action: #selector(starTapped(_:forEvent:)), for: .touchUpInside)
|
||||
button.translatesAutoresizingMaskIntoConstraints = false
|
||||
button.heightAnchor.constraint(equalToConstant: 46).isActive = true
|
||||
button.accessibilityLabel = "\(i) yıldız"
|
||||
starButtons.append(button)
|
||||
starsRow.addArrangedSubview(button)
|
||||
}
|
||||
|
||||
stack.addArrangedSubview(title)
|
||||
stack.addArrangedSubview(subtitle)
|
||||
stack.addArrangedSubview(starsRow)
|
||||
|
||||
card.addSubview(stack)
|
||||
pinCardStack(stack, in: card)
|
||||
contentStack.addArrangedSubview(card)
|
||||
}
|
||||
|
||||
private func setupCommentsSection() {
|
||||
let card = makeSectionCard()
|
||||
let stack = makeCardStack()
|
||||
|
||||
let title = UILabel()
|
||||
title.text = "Yorumlar"
|
||||
title.font = .systemFont(ofSize: 17, weight: .semibold)
|
||||
title.textColor = .white
|
||||
|
||||
commentsListStack.axis = .vertical
|
||||
commentsListStack.spacing = 10
|
||||
commentsListStack.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let composerContainer = UIView()
|
||||
composerContainer.translatesAutoresizingMaskIntoConstraints = false
|
||||
composerContainer.backgroundColor = UIColor.white.withAlphaComponent(0.06)
|
||||
composerContainer.layer.cornerRadius = 14
|
||||
composerContainer.layer.borderWidth = 1
|
||||
composerContainer.layer.borderColor = UIColor.white.withAlphaComponent(0.08).cgColor
|
||||
|
||||
commentTextView.backgroundColor = .clear
|
||||
commentTextView.textColor = .white
|
||||
commentTextView.font = .systemFont(ofSize: 14)
|
||||
commentTextView.tintColor = .systemRed
|
||||
commentTextView.textContainerInset = UIEdgeInsets(top: 10, left: 10, bottom: 10, right: 10)
|
||||
commentTextView.translatesAutoresizingMaskIntoConstraints = false
|
||||
commentTextView.delegate = self
|
||||
commentTextView.isScrollEnabled = true
|
||||
commentTextView.heightAnchor.constraint(equalToConstant: 96).isActive = true
|
||||
|
||||
commentPlaceholderLabel.text = "Yorumunu yaz..."
|
||||
commentPlaceholderLabel.textColor = UIColor.white.withAlphaComponent(0.38)
|
||||
commentPlaceholderLabel.font = .systemFont(ofSize: 14)
|
||||
commentPlaceholderLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
submitCommentButton.setTitle("Gönder", for: .normal)
|
||||
submitCommentButton.setTitleColor(.white, for: .normal)
|
||||
submitCommentButton.titleLabel?.font = .systemFont(ofSize: 14, weight: .semibold)
|
||||
submitCommentButton.backgroundColor = UIColor.systemRed.withAlphaComponent(0.92)
|
||||
submitCommentButton.layer.cornerRadius = 10
|
||||
submitCommentButton.contentEdgeInsets = UIEdgeInsets(top: 10, left: 18, bottom: 10, right: 18)
|
||||
submitCommentButton.translatesAutoresizingMaskIntoConstraints = false
|
||||
submitCommentButton.addTarget(self, action: #selector(submitCommentTapped), for: .touchUpInside)
|
||||
submitCommentButton.isEnabled = false
|
||||
submitCommentButton.alpha = 0.5
|
||||
|
||||
composerContainer.addSubview(commentTextView)
|
||||
composerContainer.addSubview(commentPlaceholderLabel)
|
||||
composerContainer.addSubview(submitCommentButton)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
commentTextView.topAnchor.constraint(equalTo: composerContainer.topAnchor),
|
||||
commentTextView.leadingAnchor.constraint(equalTo: composerContainer.leadingAnchor),
|
||||
commentTextView.trailingAnchor.constraint(equalTo: composerContainer.trailingAnchor),
|
||||
|
||||
commentPlaceholderLabel.leadingAnchor.constraint(equalTo: commentTextView.leadingAnchor, constant: 14),
|
||||
commentPlaceholderLabel.topAnchor.constraint(equalTo: commentTextView.topAnchor, constant: 12),
|
||||
|
||||
submitCommentButton.topAnchor.constraint(equalTo: commentTextView.bottomAnchor, constant: 8),
|
||||
submitCommentButton.trailingAnchor.constraint(equalTo: composerContainer.trailingAnchor, constant: -10),
|
||||
submitCommentButton.bottomAnchor.constraint(equalTo: composerContainer.bottomAnchor, constant: -10)
|
||||
])
|
||||
|
||||
stack.addArrangedSubview(title)
|
||||
stack.addArrangedSubview(commentsListStack)
|
||||
stack.addArrangedSubview(composerContainer)
|
||||
card.addSubview(stack)
|
||||
pinCardStack(stack, in: card)
|
||||
contentStack.addArrangedSubview(card)
|
||||
}
|
||||
|
||||
private func pinCardStack(_ stack: UIStackView, in card: UIView) {
|
||||
NSLayoutConstraint.activate([
|
||||
stack.topAnchor.constraint(equalTo: card.topAnchor, constant: 14),
|
||||
stack.leadingAnchor.constraint(equalTo: card.leadingAnchor, constant: 14),
|
||||
stack.trailingAnchor.constraint(equalTo: card.trailingAnchor, constant: -14),
|
||||
stack.bottomAnchor.constraint(equalTo: card.bottomAnchor, constant: -14)
|
||||
])
|
||||
}
|
||||
|
||||
private func makeSectionCard() -> UIView {
|
||||
let card = UIView()
|
||||
card.backgroundColor = UIColor.white.withAlphaComponent(0.05)
|
||||
card.layer.cornerRadius = 16
|
||||
card.layer.borderWidth = 1
|
||||
card.layer.borderColor = UIColor.white.withAlphaComponent(0.07).cgColor
|
||||
return card
|
||||
}
|
||||
|
||||
private func makeCardStack() -> UIStackView {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .vertical
|
||||
stack.spacing = 12
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
return stack
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func apply(_ state: ViewState) {
|
||||
switch state {
|
||||
|
||||
case .loading:
|
||||
spinner.startAnimating()
|
||||
messageLabel.text = "Analiz ediliyor..."
|
||||
messageLabel.isHidden = false
|
||||
overlayView.isHidden = false
|
||||
scrollView.isHidden = true
|
||||
spinner.startAnimating()
|
||||
overlayLabel.text = "İçerik analiz ediliyor..."
|
||||
|
||||
case .success(let info):
|
||||
spinner.stopAnimating()
|
||||
messageLabel.isHidden = true
|
||||
|
||||
switch info.provider {
|
||||
case "netflix":
|
||||
providerLabel.text = "NETFLIX"
|
||||
providerLabel.textColor = .systemRed
|
||||
case "primevideo":
|
||||
providerLabel.text = "PRIME VIDEO"
|
||||
providerLabel.textColor = .systemCyan
|
||||
default:
|
||||
providerLabel.text = info.provider.uppercased()
|
||||
providerLabel.textColor = .secondaryLabel
|
||||
if info.provider == "netflix" {
|
||||
providerBadge.text = " NETFLIX "
|
||||
providerBadge.backgroundColor = UIColor(red: 0.90, green: 0.11, blue: 0.15, alpha: 0.95)
|
||||
} else if info.provider == "primevideo" {
|
||||
providerBadge.text = " PRIME VIDEO "
|
||||
providerBadge.backgroundColor = UIColor(red: 0.05, green: 0.62, blue: 0.90, alpha: 0.95)
|
||||
} else {
|
||||
providerBadge.text = " \(info.provider.uppercased()) "
|
||||
providerBadge.backgroundColor = UIColor.white.withAlphaComponent(0.20)
|
||||
}
|
||||
|
||||
titleLabel.text = info.title
|
||||
|
||||
heroTitleLabel.text = info.title
|
||||
var parts: [String] = []
|
||||
if let year = info.year { parts.append("\(year)") }
|
||||
parts.append(info.type == "movie" ? "Film" : "Dizi")
|
||||
if let season = info.currentSeason { parts.append("Sezon \(season)") }
|
||||
metaLabel.text = parts.joined(separator: " · ")
|
||||
heroMetaLabel.text = parts.joined(separator: " • ")
|
||||
|
||||
genreStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
if info.genres.isEmpty {
|
||||
genresLabel.isHidden = true
|
||||
genreScroll.isHidden = true
|
||||
} else {
|
||||
genresLabel.text = info.genres.joined(separator: ", ")
|
||||
genresLabel.isHidden = false
|
||||
genreScroll.isHidden = false
|
||||
info.genres.forEach { genreStack.addArrangedSubview(makeChip($0)) }
|
||||
}
|
||||
|
||||
if let plot = info.plot, !plot.isEmpty {
|
||||
plotLabel.text = plot
|
||||
plotLabel.isHidden = false
|
||||
} else {
|
||||
plotLabel.text = nil
|
||||
plotLabel.isHidden = true
|
||||
}
|
||||
|
||||
if info.cast.isEmpty {
|
||||
castLabel.text = nil
|
||||
castLabel.isHidden = true
|
||||
} else {
|
||||
castLabel.text = "Oyuncular: \(info.cast.prefix(7).joined(separator: ", "))"
|
||||
castLabel.isHidden = false
|
||||
}
|
||||
|
||||
seedCommentsIfNeeded(for: info)
|
||||
renderComments()
|
||||
|
||||
if let urlString = info.backdrop, let imageURL = URL(string: urlString) {
|
||||
Task {
|
||||
guard let (data, _) = try? await URLSession.shared.data(from: imageURL),
|
||||
let image = UIImage(data: data) else { return }
|
||||
await MainActor.run {
|
||||
self.backdropImageView.image = image
|
||||
UIView.animate(withDuration: 0.35) {
|
||||
self.backdropImageView.alpha = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
backdropImageView.image = nil
|
||||
backdropImageView.alpha = 0
|
||||
}
|
||||
|
||||
scrollView.isHidden = false
|
||||
spinner.stopAnimating()
|
||||
UIView.animate(withDuration: 0.20, animations: {
|
||||
self.overlayView.alpha = 0
|
||||
}, completion: { _ in
|
||||
self.overlayView.isHidden = true
|
||||
self.overlayView.alpha = 1
|
||||
})
|
||||
|
||||
case .error(let message):
|
||||
spinner.stopAnimating()
|
||||
messageLabel.text = message
|
||||
messageLabel.isHidden = false
|
||||
overlayView.isHidden = false
|
||||
scrollView.isHidden = true
|
||||
spinner.stopAnimating()
|
||||
overlayLabel.text = message
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: – Share handling
|
||||
private func makeChip(_ text: String) -> UIView {
|
||||
var cfg = UIButton.Configuration.filled()
|
||||
cfg.title = text
|
||||
cfg.baseForegroundColor = .white
|
||||
cfg.baseBackgroundColor = UIColor.white.withAlphaComponent(0.10)
|
||||
cfg.cornerStyle = .capsule
|
||||
cfg.contentInsets = NSDirectionalEdgeInsets(top: 6, leading: 12, bottom: 6, trailing: 12)
|
||||
cfg.titleTextAttributesTransformer = UIConfigurationTextAttributesTransformer { attrs in
|
||||
var attrs = attrs
|
||||
attrs.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
|
||||
return attrs
|
||||
}
|
||||
let button = UIButton(configuration: cfg)
|
||||
button.isUserInteractionEnabled = false
|
||||
return button
|
||||
}
|
||||
|
||||
@objc private func starTapped(_ sender: UIButton, forEvent event: UIEvent?) {
|
||||
let touchLocation = event?.allTouches?.first?.location(in: sender) ?? CGPoint(x: sender.bounds.midX, y: sender.bounds.midY)
|
||||
let isLeftHalf = touchLocation.x < sender.bounds.midX
|
||||
let rating = Double(sender.tag - 1) + (isLeftHalf ? 0.5 : 1.0)
|
||||
updateRating(to: rating, animatedFrom: sender, withHaptic: true)
|
||||
}
|
||||
|
||||
@objc private func handleStarPan(_ gesture: UIPanGestureRecognizer) {
|
||||
let point = gesture.location(in: starsRow)
|
||||
guard starsRow.bounds.width > 0 else { return }
|
||||
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
hoverHaptic.prepare()
|
||||
fallthrough
|
||||
case .changed:
|
||||
let clampedX = min(max(point.x, 0), starsRow.bounds.width - 0.001)
|
||||
let ratio = clampedX / starsRow.bounds.width
|
||||
let starCount = Double(max(starButtons.count, 1))
|
||||
let rawValue = ratio * starCount
|
||||
let halfStepped = max(0.5, min(starCount, (rawValue * 2).rounded(.up) / 2))
|
||||
let value = halfStepped
|
||||
updateRating(to: value, animatedFrom: nil, withHaptic: true)
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func updateRating(to newValue: Double, animatedFrom sourceButton: UIButton?, withHaptic: Bool) {
|
||||
let maxRating = Double(starButtons.count)
|
||||
let clamped = min(max(newValue, 0.5), maxRating)
|
||||
guard abs(clamped - selectedRating) > 0.001 else { return }
|
||||
selectedRating = clamped
|
||||
refreshStars(animatedFrom: sourceButton)
|
||||
if withHaptic {
|
||||
hoverHaptic.impactOccurred(intensity: 1.0)
|
||||
hoverHaptic.prepare()
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshStars(animatedFrom sourceButton: UIButton? = nil) {
|
||||
let config = UIImage.SymbolConfiguration(pointSize: 30, weight: .medium)
|
||||
for button in starButtons {
|
||||
let buttonValue = Double(button.tag)
|
||||
let imageName: String
|
||||
if selectedRating >= buttonValue {
|
||||
imageName = "star.fill"
|
||||
} else if selectedRating >= (buttonValue - 0.5) {
|
||||
imageName = "star.leadinghalf.filled"
|
||||
} else {
|
||||
imageName = "star"
|
||||
}
|
||||
|
||||
button.setImage(UIImage(systemName: imageName, withConfiguration: config), for: .normal)
|
||||
button.tintColor = imageName == "star" ? UIColor.white.withAlphaComponent(0.18) : UIColor(red: 0.96, green: 0.74, blue: 0.20, alpha: 1.0)
|
||||
|
||||
let isActive = (selectedRating >= (buttonValue - 0.5))
|
||||
if isActive && sourceButton === button {
|
||||
UIView.animate(withDuration: 0.10, animations: {
|
||||
button.transform = CGAffineTransform(scaleX: 1.18, y: 1.18)
|
||||
}, completion: { _ in
|
||||
UIView.animate(withDuration: 0.10) {
|
||||
button.transform = .identity
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func setupOverlay() {
|
||||
overlayView.backgroundColor = UIColor(red: 0.04, green: 0.04, blue: 0.06, alpha: 0.96)
|
||||
overlayView.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.addSubview(overlayView)
|
||||
|
||||
spinner.color = .white
|
||||
spinner.translatesAutoresizingMaskIntoConstraints = false
|
||||
spinner.hidesWhenStopped = true
|
||||
overlayView.addSubview(spinner)
|
||||
|
||||
overlayLabel.font = .systemFont(ofSize: 15, weight: .medium)
|
||||
overlayLabel.textColor = UIColor.white.withAlphaComponent(0.82)
|
||||
overlayLabel.textAlignment = .center
|
||||
overlayLabel.numberOfLines = 0
|
||||
overlayLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
overlayView.addSubview(overlayLabel)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
overlayView.topAnchor.constraint(equalTo: headerView.bottomAnchor),
|
||||
overlayView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
|
||||
overlayView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
|
||||
overlayView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
|
||||
|
||||
spinner.centerXAnchor.constraint(equalTo: overlayView.centerXAnchor),
|
||||
spinner.centerYAnchor.constraint(equalTo: overlayView.centerYAnchor, constant: -18),
|
||||
|
||||
overlayLabel.topAnchor.constraint(equalTo: spinner.bottomAnchor, constant: 12),
|
||||
overlayLabel.leadingAnchor.constraint(equalTo: overlayView.leadingAnchor, constant: 24),
|
||||
overlayLabel.trailingAnchor.constraint(equalTo: overlayView.trailingAnchor, constant: -24)
|
||||
])
|
||||
|
||||
spinner.startAnimating()
|
||||
overlayLabel.text = "İçerik analiz ediliyor..."
|
||||
}
|
||||
|
||||
private func seedCommentsIfNeeded(for info: GetInfoResponse) {
|
||||
guard comments.isEmpty else { return }
|
||||
comments = [
|
||||
CommentItem(user: "deniz", body: "Görüntü yönetimi çok iyi, finali de güçlüydü.", time: "2 saat önce"),
|
||||
CommentItem(user: "melis", body: "Tempo bazı bölümlerde düşüyor ama genel olarak keyifli.", time: "5 saat önce"),
|
||||
CommentItem(user: "arda", body: "\(info.title) için müzik seçimleri efsane olmuş.", time: "Dün")
|
||||
]
|
||||
}
|
||||
|
||||
private func renderComments() {
|
||||
commentsListStack.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
for item in comments {
|
||||
commentsListStack.addArrangedSubview(makeCommentBubble(item))
|
||||
}
|
||||
}
|
||||
|
||||
private func makeCommentBubble(_ item: CommentItem) -> UIView {
|
||||
let bubble = UIView()
|
||||
bubble.backgroundColor = UIColor.white.withAlphaComponent(0.07)
|
||||
bubble.layer.cornerRadius = 12
|
||||
bubble.layer.borderWidth = 1
|
||||
bubble.layer.borderColor = UIColor.white.withAlphaComponent(0.06).cgColor
|
||||
|
||||
let userLabel = UILabel()
|
||||
userLabel.font = .systemFont(ofSize: 12, weight: .semibold)
|
||||
userLabel.textColor = .white
|
||||
userLabel.text = "@\(item.user)"
|
||||
userLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let bodyLabel = UILabel()
|
||||
bodyLabel.font = .systemFont(ofSize: 13, weight: .regular)
|
||||
bodyLabel.textColor = UIColor.white.withAlphaComponent(0.83)
|
||||
bodyLabel.numberOfLines = 0
|
||||
bodyLabel.text = item.body
|
||||
bodyLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
let timeLabel = UILabel()
|
||||
timeLabel.font = .systemFont(ofSize: 11, weight: .regular)
|
||||
timeLabel.textColor = UIColor.white.withAlphaComponent(0.50)
|
||||
timeLabel.text = item.time
|
||||
timeLabel.translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
bubble.addSubview(userLabel)
|
||||
bubble.addSubview(bodyLabel)
|
||||
bubble.addSubview(timeLabel)
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
userLabel.topAnchor.constraint(equalTo: bubble.topAnchor, constant: 10),
|
||||
userLabel.leadingAnchor.constraint(equalTo: bubble.leadingAnchor, constant: 12),
|
||||
userLabel.trailingAnchor.constraint(equalTo: bubble.trailingAnchor, constant: -12),
|
||||
|
||||
bodyLabel.topAnchor.constraint(equalTo: userLabel.bottomAnchor, constant: 6),
|
||||
bodyLabel.leadingAnchor.constraint(equalTo: bubble.leadingAnchor, constant: 12),
|
||||
bodyLabel.trailingAnchor.constraint(equalTo: bubble.trailingAnchor, constant: -12),
|
||||
|
||||
timeLabel.topAnchor.constraint(equalTo: bodyLabel.bottomAnchor, constant: 8),
|
||||
timeLabel.leadingAnchor.constraint(equalTo: bubble.leadingAnchor, constant: 12),
|
||||
timeLabel.trailingAnchor.constraint(equalTo: bubble.trailingAnchor, constant: -12),
|
||||
timeLabel.bottomAnchor.constraint(equalTo: bubble.bottomAnchor, constant: -10)
|
||||
])
|
||||
|
||||
return bubble
|
||||
}
|
||||
|
||||
@objc private func submitCommentTapped() {
|
||||
let text = commentTextView.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !text.isEmpty else { return }
|
||||
|
||||
comments.insert(CommentItem(user: "sen", body: text, time: "Şimdi"), at: 0)
|
||||
commentTextView.text = ""
|
||||
textViewDidChange(commentTextView)
|
||||
renderComments()
|
||||
hoverHaptic.impactOccurred(intensity: 0.35)
|
||||
hoverHaptic.prepare()
|
||||
}
|
||||
|
||||
func textViewDidChange(_ textView: UITextView) {
|
||||
commentPlaceholderLabel.isHidden = !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
let hasText = !textView.text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
submitCommentButton.alpha = hasText ? 1.0 : 0.5
|
||||
submitCommentButton.isEnabled = hasText
|
||||
}
|
||||
|
||||
@MainActor
|
||||
private func handleIncomingShare() async {
|
||||
apply(.loading)
|
||||
|
||||
let items = extensionContext?.inputItems.compactMap { $0 as? NSExtensionItem } ?? []
|
||||
let providers = items.flatMap { $0.attachments ?? [] }
|
||||
|
||||
guard !providers.isEmpty else {
|
||||
apply(.error("Paylaşılan içerik okunamadı."))
|
||||
return
|
||||
@@ -239,14 +711,13 @@ final class ShareViewController: UIViewController {
|
||||
|
||||
for provider in providers {
|
||||
if let extracted = await extractURL(from: provider), isSupportedStreamingURL(extracted) {
|
||||
// Ana uygulama arka planda açılırsa URL'i görmesi için sakla
|
||||
SharedPayloadStore.saveIncomingURL(extracted.absoluteString)
|
||||
|
||||
let normalized = normalizeURL(extracted)
|
||||
SharedPayloadStore.saveIncomingURL(normalized.absoluteString)
|
||||
do {
|
||||
let info = try await APIClient.shared.getInfo(url: extracted.absoluteString)
|
||||
let info = try await APIClient.shared.getInfo(url: normalized.absoluteString)
|
||||
apply(.success(info))
|
||||
} catch {
|
||||
apply(.error("Bilgiler alınamadı:\n\(error.localizedDescription)"))
|
||||
apply(.error("Bilgiler alınamadı.\n\(error.localizedDescription)"))
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -255,13 +726,37 @@ final class ShareViewController: UIViewController {
|
||||
apply(.error("Geçerli bir Netflix/Prime Video linki bulunamadı."))
|
||||
}
|
||||
|
||||
// MARK: – Actions
|
||||
|
||||
@objc private func closeTapped() {
|
||||
extensionContext?.completeRequest(returningItems: nil, completionHandler: nil)
|
||||
}
|
||||
|
||||
// MARK: – URL helpers
|
||||
@objc private func handleDismissPan(_ gesture: UIPanGestureRecognizer) {
|
||||
let translation = gesture.translation(in: view)
|
||||
let velocity = gesture.velocity(in: view)
|
||||
|
||||
switch gesture.state {
|
||||
case .began:
|
||||
dismissPanStartTransform = headerView.transform
|
||||
case .changed:
|
||||
let downY = max(0, translation.y)
|
||||
let progress = min(downY / 140.0, 1.0)
|
||||
headerView.transform = dismissPanStartTransform.translatedBy(x: 0, y: downY * 0.3)
|
||||
headerView.alpha = 1.0 - (progress * 0.25)
|
||||
case .ended, .cancelled, .failed:
|
||||
let shouldDismiss = translation.y > 90 || velocity.y > 900
|
||||
if shouldDismiss {
|
||||
closeTapped()
|
||||
return
|
||||
}
|
||||
|
||||
UIView.animate(withDuration: 0.18) {
|
||||
self.headerView.transform = .identity
|
||||
self.headerView.alpha = 1.0
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
private func extractURL(from provider: NSItemProvider) async -> URL? {
|
||||
if provider.hasItemConformingToTypeIdentifier(UTType.url.identifier) {
|
||||
@@ -295,13 +790,24 @@ final class ShareViewController: UIViewController {
|
||||
return nil
|
||||
}
|
||||
|
||||
private func normalizeURL(_ url: URL) -> URL {
|
||||
let host = url.host?.lowercased() ?? ""
|
||||
guard host == "app.primevideo.com" else { return url }
|
||||
var components = URLComponents(url: url, resolvingAgainstBaseURL: false)
|
||||
if let gti = components?.queryItems?.first(where: { $0.name == "gti" }) {
|
||||
components?.queryItems = [gti]
|
||||
} else {
|
||||
components?.queryItems = nil
|
||||
}
|
||||
return components?.url ?? url
|
||||
}
|
||||
|
||||
private func isSupportedStreamingURL(_ url: URL) -> Bool {
|
||||
let host = url.host?.lowercased() ?? ""
|
||||
let netflixHosts = ["www.netflix.com", "netflix.com", "www.netflix.com.tr", "netflix.com.tr"]
|
||||
let primeHosts = ["www.primevideo.com", "primevideo.com", "www.amazon.com", "amazon.com"]
|
||||
let primeHosts = ["www.primevideo.com", "primevideo.com", "www.amazon.com", "amazon.com", "app.primevideo.com"]
|
||||
|
||||
guard netflixHosts.contains(host) || primeHosts.contains(host) else { return false }
|
||||
|
||||
let path = url.path.lowercased()
|
||||
if path.contains("/title/") || path.contains("/watch/") || path.contains("/detail/") {
|
||||
return true
|
||||
@@ -311,11 +817,12 @@ final class ShareViewController: UIViewController {
|
||||
|
||||
private static func firstURL(in raw: String) -> URL? {
|
||||
let text = raw.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if let url = URL(string: text), url.scheme?.isEmpty == false { return url }
|
||||
if let url = URL(string: text), url.scheme?.isEmpty == false {
|
||||
return url
|
||||
}
|
||||
guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else {
|
||||
return nil
|
||||
}
|
||||
let range = NSRange(text.startIndex..<text.endIndex, in: text)
|
||||
return detector.firstMatch(in: text, options: [], range: range)?.url
|
||||
return detector.firstMatch(in: text, options: [], range: NSRange(text.startIndex..., in: text))?.url
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ const NETFLIX_HOSTS = new Set([
|
||||
const PRIME_HOSTS = new Set([
|
||||
'www.primevideo.com',
|
||||
'primevideo.com',
|
||||
'app.primevideo.com', // iOS uygulama paylaşım linkleri
|
||||
]);
|
||||
|
||||
export interface ParsedContentUrl {
|
||||
@@ -31,11 +32,20 @@ export function parseSupportedContentUrl(rawUrl: string): ParsedContentUrl | nul
|
||||
}
|
||||
|
||||
if (PRIME_HOSTS.has(hostname)) {
|
||||
const detailIdMatch = parsedUrl.pathname.match(/\/detail\/([A-Za-z0-9]+)/);
|
||||
if (!detailIdMatch) return null;
|
||||
const id = detailIdMatch[1];
|
||||
if (!id) return null;
|
||||
return { provider: 'primevideo', id };
|
||||
// Standart web URL: /detail/TITLE/ID veya /-/tr/detail/ID
|
||||
// GTI formatı nokta ve tire içerebilir: amzn1.dv.gti.UUID
|
||||
const detailIdMatch = parsedUrl.pathname.match(/\/detail\/([A-Za-z0-9._-]+)/);
|
||||
if (detailIdMatch?.[1]) {
|
||||
return { provider: 'primevideo', id: detailIdMatch[1] };
|
||||
}
|
||||
|
||||
// iOS uygulama paylaşım linki: /detail?gti=amzn1.dv.gti.UUID
|
||||
if (parsedUrl.pathname === '/detail') {
|
||||
const gti = parsedUrl.searchParams.get('gti');
|
||||
if (gti) return { provider: 'primevideo', id: gti };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
|
||||
Reference in New Issue
Block a user