import SwiftUI import UIKit struct ContentView: View { @StateObject private var viewModel = MainViewModel() @Environment(\.scenePhase) private var scenePhase @State private var selectedRating: Double = 0 @State private var commentDraft = "" @State private var comments: [CommentItem] = [] @State private var interactionKey = "" private let hoverHaptic = UIImpactFeedbackGenerator(style: .light) private let submitHaptic = UIImpactFeedbackGenerator(style: .medium) var body: some View { NavigationStack { ZStack { LinearGradient( colors: [ Color(red: 0.05, green: 0.05, blue: 0.07), Color(red: 0.02, green: 0.02, blue: 0.03) ], startPoint: .top, endPoint: .bottom ) .ignoresSafeArea() ScrollView(showsIndicators: false) { VStack(spacing: 16) { urlComposerCard if viewModel.isLoading { loadingCard } if let error = viewModel.errorMessage { errorCard(error) } if let result = viewModel.result { resultCard(result) ratingCard commentsCard } else if !viewModel.isLoading { emptyCard } } .padding(.horizontal, 14) .padding(.vertical, 14) } } .toolbar { ToolbarItem(placement: .principal) { Text("Ratebubble") .font(.headline) .foregroundStyle(.white) } } } .preferredColorScheme(.dark) .onAppear { hoverHaptic.prepare() submitHaptic.prepare() viewModel.consumeSharedURLIfAny() } .onOpenURL { _ in viewModel.consumeSharedURLIfAny() } .onChange(of: scenePhase) { phase in if phase == .active { viewModel.consumeSharedURLIfAny() } } } private var urlComposerCard: some View { card { VStack(alignment: .leading, spacing: 12) { Text("Paylaşılan Link") .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) HStack(spacing: 10) { Image(systemName: "link") .foregroundStyle(.white.opacity(0.65)) TextField("https://www.netflix.com/title/...", text: $viewModel.sharedURL) .textInputAutocapitalization(.never) .autocorrectionDisabled(true) .keyboardType(.URL) .foregroundStyle(.white) } .padding(.horizontal, 12) .padding(.vertical, 11) .background(Color.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 12)) Button { Task { await viewModel.fetch() } } label: { HStack { Spacer() Text(viewModel.isLoading ? "Analiz Ediliyor..." : "İçeriği Analiz Et") .font(.subheadline.weight(.semibold)) Spacer() } .padding(.vertical, 11) .background( LinearGradient( colors: [Color.red.opacity(0.95), Color.orange.opacity(0.9)], startPoint: .leading, endPoint: .trailing ), in: RoundedRectangle(cornerRadius: 12) ) } .disabled(viewModel.isLoading) .foregroundStyle(.white) .opacity(viewModel.isLoading ? 0.7 : 1) } } } private var loadingCard: some View { card { HStack(spacing: 12) { ProgressView() Text("İçerik çözülüyor ve metadata hazırlanıyor...") .font(.footnote) .foregroundStyle(.white.opacity(0.8)) } .frame(maxWidth: .infinity, alignment: .leading) } } private func errorCard(_ message: String) -> some View { card { VStack(alignment: .leading, spacing: 8) { Text("Bir sorun oluştu") .font(.subheadline.weight(.semibold)) .foregroundStyle(.red.opacity(0.95)) Text(message) .font(.footnote) .foregroundStyle(.white.opacity(0.82)) } .frame(maxWidth: .infinity, alignment: .leading) } } private func resultCard(_ result: GetInfoResponse) -> some View { let key = contentKey(result) return card { VStack(alignment: .leading, spacing: 10) { providerBadge(result.provider) Text(result.title) .font(.title3.weight(.bold)) .foregroundStyle(.white) .lineLimit(3) Text(metaLine(result)) .font(.footnote) .foregroundStyle(.white.opacity(0.65)) if !result.genres.isEmpty { ScrollView(.horizontal, showsIndicators: false) { HStack(spacing: 8) { ForEach(result.genres, id: \.self) { genre in Text(genre) .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 6) .background(Color.white.opacity(0.09), in: Capsule()) } } } } if let plot = result.plot, !plot.isEmpty { Text(plot) .font(.footnote) .foregroundStyle(.white.opacity(0.84)) .lineLimit(6) } } .onAppear { prepareInteractionState(for: result, key: key) } .onChange(of: key) { _ in prepareInteractionState(for: result, key: key) } } } private var ratingCard: some View { card { VStack(alignment: .leading, spacing: 10) { Text("Puanla") .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) Text("Yarım yıldız da seçebilirsin. Sürükleyerek hızlı puan ver.") .font(.caption) .foregroundStyle(.white.opacity(0.6)) StarRatingBar( rating: $selectedRating, onChanged: { changed in guard changed else { return } hoverHaptic.impactOccurred(intensity: 0.8) hoverHaptic.prepare() } ) .frame(height: 46) } .frame(maxWidth: .infinity, alignment: .leading) } } private var commentsCard: some View { card { VStack(alignment: .leading, spacing: 12) { Text("Yorumlar") .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) if comments.isEmpty { Text("Henüz yorum yok. İlk yorumu sen yaz.") .font(.footnote) .foregroundStyle(.white.opacity(0.58)) } else { ForEach(comments) { item in CommentBubble(item: item) } } VStack(alignment: .trailing, spacing: 8) { ZStack(alignment: .topLeading) { TextEditor(text: $commentDraft) .scrollContentBackground(.hidden) .foregroundStyle(.white) .frame(minHeight: 88, maxHeight: 120) .padding(8) .background(Color.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 12)) if commentDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { Text("Yorumunu yaz...") .font(.footnote) .foregroundStyle(.white.opacity(0.38)) .padding(.horizontal, 18) .padding(.vertical, 18) .allowsHitTesting(false) } } Button { submitComment() } label: { Text("Gönder") .font(.footnote.weight(.semibold)) .padding(.horizontal, 16) .padding(.vertical, 9) .background(Color.red.opacity(0.9), in: Capsule()) } .disabled(commentDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty) .opacity(commentDraft.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? 0.45 : 1) .foregroundStyle(.white) } } .frame(maxWidth: .infinity, alignment: .leading) } } private var emptyCard: some View { card { VStack(alignment: .leading, spacing: 8) { Text("Hazır") .font(.subheadline.weight(.semibold)) .foregroundStyle(.white) Text("Paylaşımdan gelen bir içerik varsa otomatik doldurulur. İstersen yukarıdan URL girip analiz başlat.") .font(.footnote) .foregroundStyle(.white.opacity(0.66)) } .frame(maxWidth: .infinity, alignment: .leading) } } private func providerBadge(_ provider: String) -> some View { Text(provider.uppercased()) .font(.caption2.weight(.bold)) .foregroundStyle(.white) .padding(.horizontal, 9) .padding(.vertical, 5) .background( provider == "netflix" ? Color.red.opacity(0.92) : (provider == "primevideo" ? Color.cyan.opacity(0.82) : Color.white.opacity(0.2)), in: Capsule() ) } private func metaLine(_ result: GetInfoResponse) -> String { var parts: [String] = [] if let year = result.year { parts.append(String(year)) } parts.append(result.type == "movie" ? "Film" : "Dizi") if let currentSeason = result.currentSeason { parts.append("Sezon \(currentSeason)") } return parts.joined(separator: " • ") } private func contentKey(_ result: GetInfoResponse) -> String { "\(result.provider)|\(result.title)|\(result.year.map(String.init) ?? "-")" } private func prepareInteractionState(for result: GetInfoResponse, key: String) { guard interactionKey != key else { return } interactionKey = key selectedRating = 0 commentDraft = "" comments = [ CommentItem(user: "deniz", body: "Sinematografi çok temiz, finali de iyi bağlamışlar.", time: "2 saat önce"), CommentItem(user: "melis", body: "\(result.title) için tempo yer yer düşse de genel deneyim çok keyifli.", time: "Dün") ] } private func submitComment() { let trimmed = commentDraft.trimmingCharacters(in: .whitespacesAndNewlines) guard !trimmed.isEmpty else { return } comments.insert(CommentItem(user: "sen", body: trimmed, time: "Şimdi"), at: 0) commentDraft = "" submitHaptic.impactOccurred(intensity: 0.5) submitHaptic.prepare() } private func card(@ViewBuilder _ content: () -> Content) -> some View { content() .padding(14) .frame(maxWidth: .infinity, alignment: .leading) .background( RoundedRectangle(cornerRadius: 16) .fill(Color.white.opacity(0.06)) .overlay( RoundedRectangle(cornerRadius: 16) .stroke(Color.white.opacity(0.08), lineWidth: 1) ) ) } } private struct CommentItem: Identifiable { let id = UUID() let user: String let body: String let time: String } private struct CommentBubble: View { let item: CommentItem var body: some View { VStack(alignment: .leading, spacing: 6) { Text("@\(item.user)") .font(.caption.weight(.semibold)) .foregroundStyle(.white) Text(item.body) .font(.footnote) .foregroundStyle(.white.opacity(0.82)) Text(item.time) .font(.caption2) .foregroundStyle(.white.opacity(0.5)) } .frame(maxWidth: .infinity, alignment: .leading) .padding(10) .background(Color.white.opacity(0.06), in: RoundedRectangle(cornerRadius: 12)) } } private struct StarRatingBar: View { @Binding var rating: Double let onChanged: (Bool) -> Void var body: some View { GeometryReader { geo in HStack(spacing: 4) { ForEach(1...5, id: \.self) { idx in Image(systemName: iconName(for: idx)) .font(.system(size: 30, weight: .medium)) .foregroundStyle(iconColor(for: idx)) .frame(maxWidth: .infinity, maxHeight: .infinity) } } .contentShape(Rectangle()) .gesture( DragGesture(minimumDistance: 0) .onChanged { value in let changed = updateRating(from: value.location.x, width: geo.size.width) onChanged(changed) } ) } .accessibilityElement(children: .ignore) .accessibilityLabel("Puan") .accessibilityValue("\(String(format: "%.1f", rating)) yıldız") } private func iconName(for index: Int) -> String { let value = Double(index) if rating >= value { return "star.fill" } if rating >= value - 0.5 { return "star.leadinghalf.filled" } return "star" } private func iconColor(for index: Int) -> Color { let value = Double(index) return rating >= value - 0.5 ? Color(red: 0.96, green: 0.74, blue: 0.20) : Color.white.opacity(0.20) } @discardableResult private func updateRating(from x: CGFloat, width: CGFloat) -> Bool { guard width > 0 else { return false } let clampedX = min(max(x, 0), width - 0.001) let raw = (clampedX / width) * 5.0 let stepped = max(0.5, min(5.0, (raw * 2).rounded(.up) / 2)) guard abs(stepped - rating) > 0.001 else { return false } rating = stepped return true } }