Files
ratebubble/ios/Ratebubble/ShareExtension/ShareViewController.swift

322 lines
12 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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
}
}