import UIKit import UniformTypeIdentifiers final class ShareViewController: UIViewController, UITextViewDelegate { private enum ViewState { case loading case success(GetInfoResponse) case error(String) } private struct CommentItem { let user: String let body: String let time: String } private var selectedRating = 0.0 private var starButtons: [UIButton] = [] private var comments: [CommentItem] = [] private let headerView = UIView() private let headerLabel = UILabel() private let closeButton = UIButton(type: .system) private let overlayView = UIView() private let spinner = UIActivityIndicatorView(style: .large) private let overlayLabel = UILabel() private let scrollView = UIScrollView() private 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 genreScroll = UIScrollView() private let genreStack = UIStackView() private let plotLabel = UILabel() private let castLabel = UILabel() 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() 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() setupScrollView() setupOverlay() setupFeedback() Task { await handleIncomingShare() } } override func viewDidLayoutSubviews() { super.viewDidLayoutSubviews() gradientLayer.frame = backdropContainer.bounds } private func setupFeedback() { hoverHaptic.prepare() } private func setupHeader() { 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.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 = UIColor.white.withAlphaComponent(0.08) separator.translatesAutoresizingMaskIntoConstraints = false headerView.addSubview(separator) NSLayoutConstraint.activate([ headerView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor), headerView.leadingAnchor.constraint(equalTo: view.leadingAnchor), headerView.trailingAnchor.constraint(equalTo: view.trailingAnchor), headerView.heightAnchor.constraint(equalToConstant: 46), headerLabel.centerXAnchor.constraint(equalTo: headerView.centerXAnchor), headerLabel.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), closeButton.trailingAnchor.constraint(equalTo: headerView.trailingAnchor, constant: -16), closeButton.centerYAnchor.constraint(equalTo: headerView.centerYAnchor), separator.leadingAnchor.constraint(equalTo: headerView.leadingAnchor), separator.trailingAnchor.constraint(equalTo: headerView.trailingAnchor), separator.bottomAnchor.constraint(equalTo: headerView.bottomAnchor), separator.heightAnchor.constraint(equalToConstant: 0.5) ]) } private func setupScrollView() { scrollView.alwaysBounceVertical = true scrollView.keyboardDismissMode = .interactive scrollView.translatesAutoresizingMaskIntoConstraints = false scrollView.isHidden = true view.addSubview(scrollView) 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) ]) 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) ]) } 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: overlayView.isHidden = false scrollView.isHidden = true spinner.startAnimating() overlayLabel.text = "İçerik analiz ediliyor..." case .success(let info): 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) } 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)") } heroMetaLabel.text = parts.joined(separator: " • ") genreStack.arrangedSubviews.forEach { $0.removeFromSuperview() } if info.genres.isEmpty { genreScroll.isHidden = true } else { 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): overlayView.isHidden = false scrollView.isHidden = true spinner.stopAnimating() overlayLabel.text = message } } 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 } for provider in providers { if let extracted = await extractURL(from: provider), isSupportedStreamingURL(extracted) { let normalized = normalizeURL(extracted) SharedPayloadStore.saveIncomingURL(normalized.absoluteString) do { let info = try await APIClient.shared.getInfo(url: normalized.absoluteString) apply(.success(info)) } catch { apply(.error("Bilgiler alınamadı.\n\(error.localizedDescription)")) } return } } apply(.error("Geçerli bir Netflix/Prime Video linki bulunamadı.")) } @objc private func closeTapped() { extensionContext?.completeRequest(returningItems: nil, completionHandler: nil) } @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) { return await withCheckedContinuation { continuation in provider.loadItem(forTypeIdentifier: UTType.url.identifier, options: nil) { item, _ in if let url = item as? URL { continuation.resume(returning: url) return } if let raw = item as? String { continuation.resume(returning: Self.firstURL(in: raw)) return } continuation.resume(returning: nil) } } } if provider.hasItemConformingToTypeIdentifier(UTType.text.identifier) { return await withCheckedContinuation { continuation in provider.loadItem(forTypeIdentifier: UTType.text.identifier, options: nil) { item, _ in if let raw = item as? String, let url = Self.firstURL(in: raw) { continuation.resume(returning: url) return } continuation.resume(returning: nil) } } } 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", "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 } return !path.isEmpty && path != "/" } 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 } guard let detector = try? NSDataDetector(types: NSTextCheckingResult.CheckingType.link.rawValue) else { return nil } return detector.firstMatch(in: text, options: [], range: NSRange(text.startIndex..., in: text))?.url } }