feat(UI): ios için güncellemeler içerir.
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -14,6 +14,14 @@
|
||||
<string>$(PRODUCT_NAME)</string>
|
||||
<key>CFBundleDisplayName</key>
|
||||
<string>Ratebubble Share</string>
|
||||
<key>API_BASE_URL</key>
|
||||
<string>$(API_BASE_URL)</string>
|
||||
<key>MOBILE_API_KEY</key>
|
||||
<string>$(MOBILE_API_KEY)</string>
|
||||
<key>APP_GROUP_ID</key>
|
||||
<string>$(APP_GROUP_ID)</string>
|
||||
<key>APP_URL_SCHEME</key>
|
||||
<string>$(APP_URL_SCHEME)</string>
|
||||
<key>CFBundlePackageType</key>
|
||||
<string>XPC!</string>
|
||||
<key>CFBundleShortVersionString</key>
|
||||
|
||||
@@ -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..<text.endIndex, in: text)
|
||||
return detector.firstMatch(in: text, options: [], range: range)?.url
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user