import UIKit import UniformTypeIdentifiers final class ShareViewController: UIViewController { // MARK: – View state private enum ViewState { case loading case success(GetInfoResponse) case error(String) } // MARK: – Header private let headerView = UIView() private let headerLabel = UILabel() private let closeButton = UIButton(type: .system) // MARK: – Loading / error private let spinner = UIActivityIndicatorView(style: .large) private let messageLabel = UILabel() // MARK: – Metadata card (genişletilebilir — buraya puan/yorum ekleyebilirsin) private let scrollView = UIScrollView() /// Bu stack'e ileride yıldız seçici, yorum alanı vb. ekleyebilirsin. let contentStack = UIStackView() private let providerLabel = UILabel() private let titleLabel = UILabel() private let metaLabel = UILabel() private let genresLabel = UILabel() private let plotLabel = UILabel() // MARK: – Lifecycle override func viewDidLoad() { super.viewDidLoad() view.backgroundColor = .systemBackground setupHeader() setupLoadingViews() setupScrollView() Task { await handleIncomingShare() } } // MARK: – Setup private func setupHeader() { headerView.backgroundColor = .systemBackground headerView.translatesAutoresizingMaskIntoConstraints = false view.addSubview(headerView) headerLabel.text = "Ratebubble" headerLabel.font = .systemFont(ofSize: 17, weight: .semibold) headerLabel.translatesAutoresizingMaskIntoConstraints = false headerView.addSubview(headerLabel) closeButton.setTitle("Kapat", for: .normal) closeButton.addTarget(self, action: #selector(closeTapped), for: .touchUpInside) closeButton.translatesAutoresizingMaskIntoConstraints = false headerView.addSubview(closeButton) let separator = UIView() separator.backgroundColor = .separator 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: 44), 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 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.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), 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), ]) } // MARK: – State application @MainActor private func apply(_ state: ViewState) { switch state { case .loading: spinner.startAnimating() messageLabel.text = "Analiz ediliyor..." messageLabel.isHidden = false scrollView.isHidden = true 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 } titleLabel.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: " · ") if info.genres.isEmpty { genresLabel.isHidden = true } else { genresLabel.text = info.genres.joined(separator: ", ") genresLabel.isHidden = false } if let plot = info.plot, !plot.isEmpty { plotLabel.text = plot plotLabel.isHidden = false } else { plotLabel.isHidden = true } scrollView.isHidden = false case .error(let message): spinner.stopAnimating() messageLabel.text = message messageLabel.isHidden = false scrollView.isHidden = true } } // MARK: – Share handling @MainActor private func handleIncomingShare() async { 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) { // Ana uygulama arka planda açılırsa URL'i görmesi için sakla SharedPayloadStore.saveIncomingURL(extracted.absoluteString) do { let info = try await APIClient.shared.getInfo(url: extracted.absoluteString) apply(.success(info)) } catch { apply(.error("Bilgiler alınamadı:\n\(error.localizedDescription)")) } return } } 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 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 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"] 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 } let range = NSRange(text.startIndex..