From 8bd4f24774c22a1b663e3899cfad5b8433de83d4 Mon Sep 17 00:00:00 2001 From: wisecolt Date: Tue, 3 Mar 2026 22:50:14 +0300 Subject: [PATCH] =?UTF-8?q?feat(UI):=20ios=20i=C3=A7in=20g=C3=BCncellemele?= =?UTF-8?q?r=20i=C3=A7erir.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../UserInterfaceState.xcuserstate | Bin 0 -> 24752 bytes ios/Ratebubble/Resources/Config.xcconfig | 4 +- .../Resources/RatebubbleShare-Info.plist | 8 + .../ShareExtension/ShareViewController.swift | 294 +++++++++++++++--- 4 files changed, 264 insertions(+), 42 deletions(-) create mode 100644 ios/Ratebubble.xcodeproj/project.xcworkspace/xcuserdata/wisecolt-macmini.xcuserdatad/UserInterfaceState.xcuserstate 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 0000000000000000000000000000000000000000..f1d9b417d2995bb58678420de4a511ee0fde405c GIT binary patch literal 24752 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

f^z`)1^qlm(^n!G2dUN`m^m*yd^!D@x z>7D6kr=Op`CH=1S=hI(Ee<}Sy`YY+LrN5E>R{A^X@1-9~Kb-zi`o|eKgU(_gdyvp>q2o>P-k zn^T{2X3p(759hp;b0FuHoY!*R$ayR0ot*b`4(A-n`84N`T$D@YGPzvtgxra_p}FC? zk-5>ivALRDd+y@g4Y`lx9?s+Pvhs|1uDrAJy7T(-`tw%gU68jrZ%y7Md6(r~k#~LG z=Dgv&t$DZR-JW+>-mbit^1jbU`D}i0{u%k<`4Rb1`7!yK`RaUK{*?Ub`TBfIeocN| zenY-Je?|Vz{1@_H&;Ka@=lov_sDki<^n%O+bwOEyu3$<*ML}hOvA|r=RM1+mq+of$ zIR&I(Rl(|liwiC*SX*#)!LOI;E{sI3w9UmDL7Q{S7An> zsnAonws2SBfx@GTFh!gqQIVpUq{vmM6&gjULaQiOOi@@AwTcFXLorJ+M=@V8W?3zZ6OVg@p)3`PL8n4EuS*%&5xj=KF<|55C zn)RCNG@CRxX$Cd7Ywp)Pq~b-QtcJmtF+f>*K0RwZ_sYm4r{k+w`*_H-l4r)d$0BZ?Zeu~w7a!W zY4>XPY4>Yi(jL^lrhQZUj`n@+VeJv^r`j*HUu%zQf6)G{{g?I+9n#S{PB%dpq6^nW z>SA>9xYwGvmq`*Ui{8hbqT=7lCclvUDrQW2!RDX?rz5Xrzhx(5z^D2ugODlbq=Twr)hby0| z+-r~-5)8?P4nv>8Yk17C*Rao+V$3$?8GDRNjLVIW8lN#fYf3fcnDR|(WT7pbWr8Kd5@t!Tq+9YW8cUgFhQ(~DvCOi}vCOx)EbW#BmPMB3mUAo* zS@v2ESYEZfVR_r~p5+6}mzHlV-&uaN{9^gd@`rV*wbOdD^>B4)b!_#d>h$V@>XK@0 zbw#zc+E(3EJ-^yjeRj3CdU5rt>J8P`SKnAYSiPlsTlJ3Woz-_%@2bA9`nl@;)h|^a zsD7pT_3F2(->p7W{bBXT)t^>>UvoxHM$Ock`kDnbD{3yR*;sRT%~Lh6+2U*|HjQnv z&1AFN=Gf-jT()*whpo@%wJo+Sv#qq9Yg=!-+qSu?=YH?c0PE}|~FF1Bt` zU3Q(aPHQ*Yt#-TpEPIE&$G*b8#(t^&3j0;|YwYXo8|~ZdJM25{ciMN^@3TK>f7pJ= n!8(+V7DuaNp~L4`;#loi>$p<#nJ7y7&7C*mVvhWBT>XCllKkZ~ literal 0 HcmV?d00001 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..