322 lines
12 KiB
Swift
322 lines
12 KiB
Swift
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..<text.endIndex, in: text)
|
||
return detector.firstMatch(in: text, options: [], range: range)?.url
|
||
}
|
||
}
|