diff --git a/ios/Ratebubble.xcodeproj/project.xcworkspace/xcuserdata/wisecolt-macmini.xcuserdatad/UserInterfaceState.xcuserstate b/ios/Ratebubble.xcodeproj/project.xcworkspace/xcuserdata/wisecolt-macmini.xcuserdatad/UserInterfaceState.xcuserstate
new file mode 100644
index 0000000..f1d9b41
Binary files /dev/null and b/ios/Ratebubble.xcodeproj/project.xcworkspace/xcuserdata/wisecolt-macmini.xcuserdatad/UserInterfaceState.xcuserstate differ
diff --git a/ios/Ratebubble/Resources/Config.xcconfig b/ios/Ratebubble/Resources/Config.xcconfig
index 027fa81..e6a49a2 100644
--- a/ios/Ratebubble/Resources/Config.xcconfig
+++ b/ios/Ratebubble/Resources/Config.xcconfig
@@ -1,5 +1,5 @@
SLASH = /
-API_BASE_URL = http:$(SLASH)$(SLASH)localhost:3000
-MOBILE_API_KEY = mobile-dev-key-change-me
+API_BASE_URL = http:$(SLASH)$(SLASH)192.168.1.124:3000
+MOBILE_API_KEY = mobile-app-key-change-me-in-production
APP_GROUP_ID = group.net.wisecolt.ratebubble
APP_URL_SCHEME = ratebubble
diff --git a/ios/Ratebubble/Resources/RatebubbleShare-Info.plist b/ios/Ratebubble/Resources/RatebubbleShare-Info.plist
index 57404f2..5f65ef7 100644
--- a/ios/Ratebubble/Resources/RatebubbleShare-Info.plist
+++ b/ios/Ratebubble/Resources/RatebubbleShare-Info.plist
@@ -14,6 +14,14 @@
$(PRODUCT_NAME)
CFBundleDisplayName
Ratebubble Share
+ API_BASE_URL
+ $(API_BASE_URL)
+ MOBILE_API_KEY
+ $(MOBILE_API_KEY)
+ APP_GROUP_ID
+ $(APP_GROUP_ID)
+ APP_URL_SCHEME
+ $(APP_URL_SCHEME)
CFBundlePackageType
XPC!
CFBundleShortVersionString
diff --git a/ios/Ratebubble/ShareExtension/ShareViewController.swift b/ios/Ratebubble/ShareExtension/ShareViewController.swift
index 3f096c5..cf8047e 100644
--- a/ios/Ratebubble/ShareExtension/ShareViewController.swift
+++ b/ios/Ratebubble/ShareExtension/ShareViewController.swift
@@ -2,58 +2,280 @@ import UIKit
import UniformTypeIdentifiers
final class ShareViewController: UIViewController {
- private let statusLabel = UILabel()
+
+ // 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()
- setupUI()
+ view.backgroundColor = .systemBackground
+ setupHeader()
+ setupLoadingViews()
+ setupScrollView()
Task { await handleIncomingShare() }
}
- private func setupUI() {
- view.backgroundColor = .systemBackground
- statusLabel.translatesAutoresizingMaskIntoConstraints = false
- statusLabel.textAlignment = .center
- statusLabel.numberOfLines = 0
- statusLabel.text = "Paylaşılan bağlantı alınıyor..."
+ // 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)
- view.addSubview(statusLabel)
NSLayoutConstraint.activate([
- statusLabel.leadingAnchor.constraint(equalTo: view.leadingAnchor, constant: 20),
- statusLabel.trailingAnchor.constraint(equalTo: view.trailingAnchor, constant: -20),
- statusLabel.centerYAnchor.constraint(equalTo: view.centerYAnchor)
+ 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),
])
}
- @MainActor
- private func updateStatus(_ text: String) {
- statusLabel.text = text
+ 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 {
- guard let item = extensionContext?.inputItems.first as? NSExtensionItem,
- let providers = item.attachments else {
- updateStatus("Paylaşılan içerik okunamadı.")
+ 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)
- updateStatus("Bağlantı alındı, uygulama açılıyor...")
- openHostApp()
+
+ do {
+ let info = try await APIClient.shared.getInfo(url: extracted.absoluteString)
+ apply(.success(info))
+ } catch {
+ apply(.error("Bilgiler alınamadı:\n\(error.localizedDescription)"))
+ }
return
}
}
- updateStatus("Geçerli bir Netflix/Prime Video linki bulunamadı.")
+ 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
- continuation.resume(returning: item as? URL)
+ 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)
}
}
}
@@ -61,7 +283,7 @@ final class ShareViewController: UIViewController {
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 = URL(string: raw.trimmingCharacters(in: .whitespacesAndNewlines)) {
+ if let raw = item as? String, let url = Self.firstURL(in: raw) {
continuation.resume(returning: url)
return
}
@@ -76,32 +298,24 @@ final class ShareViewController: UIViewController {
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"]
- let isNetflix = netflixHosts.contains(host)
- let isPrime = primeHosts.contains(host)
- guard isNetflix || isPrime else { return false }
+ 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
}
-
- // Some share links can be shortened/redirect style without a canonical path.
return !path.isEmpty && path != "/"
}
- private func openHostApp() {
- guard let url = URL(string: "\(SharedConfig.appURLScheme)://ingest") else {
- extensionContext?.completeRequest(returningItems: nil)
- return
- }
-
- extensionContext?.open(url) { success in
- // If opening succeeded, the system should transition to the host app.
- // Completing the extension request immediately can bounce back to the source app.
- guard !success else { return }
- self.extensionContext?.completeRequest(returningItems: nil)
+ 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..