import Foundation import Combine final class MainContentAllViewModel: ObservableObject { private let repository = MainContentAllRepository() private var subscription = Set() private let pageSize = 20 private var latestRequestId = 0 @Published var selectedType: MainContentAllType = .audio @Published var selectedSort: ContentSort = .latest @Published var selectedDayOfWeek: SeriesPublishedDaysOfWeek = .currentDeviceDay @Published var response: MainContentAllTabResponse? @Published var audios = [MainContentAudioResponse]() @Published var series = [MainContentSeriesResponse]() @Published var page = 0 @Published var size = 20 @Published var hasNext = false @Published var hasLoaded = false @Published var isLoading = false @Published var isLoadingNextPage = false @Published var errorMessage = "" @Published var isShowPopup = false var allowedSorts: [ContentSort] { selectedType == .free ? [.latest, .popular] : ContentSort.allCases } var shouldShowEmptyState: Bool { guard hasLoaded, isLoading == false else { return false } return selectedType.usesAudioItems ? audios.isEmpty : series.isEmpty } func fetchFirstPageIfNeeded() { guard hasLoaded == false else { return } fetchFirstPage() } func fetchFirstPage() { fetchContents(page: 0, isNextPage: false) } func selectType(_ type: MainContentAllType) { guard selectedType != type else { return } selectedType = type if allowedSorts.contains(selectedSort) == false { selectedSort = .latest } clearItems() fetchFirstPage() } func selectSort(_ sort: ContentSort) { guard allowedSorts.contains(sort), selectedSort != sort else { return } selectedSort = sort clearItems() fetchFirstPage() } func selectDayOfWeek(_ dayOfWeek: SeriesPublishedDaysOfWeek) { guard selectedType == .series, selectedDayOfWeek != dayOfWeek else { return } selectedDayOfWeek = dayOfWeek clearItems() fetchFirstPage() } func fetchNextPageIfNeeded(currentAudio: MainContentAudioResponse) { guard audios.last?.audioContentId == currentAudio.audioContentId else { return } fetchNextPageIfPossible() } func fetchNextPageIfNeeded(currentSeries: MainContentSeriesResponse) { guard series.last?.seriesId == currentSeries.seriesId else { return } fetchNextPageIfPossible() } private func fetchNextPageIfPossible() { guard hasNext, isLoading == false, isLoadingNextPage == false else { return } fetchContents(page: page + 1, isNextPage: true) } private func fetchContents(page requestPage: Int, isNextPage: Bool) { latestRequestId += 1 let requestId = latestRequestId if isNextPage { isLoadingNextPage = true } else { isLoading = true } repository.getContents( type: selectedType, sort: selectedSort, page: requestPage, size: pageSize, dayOfWeek: selectedDayOfWeek ) .sink { [weak self] result in guard let self else { return } guard requestId == self.latestRequestId else { return } switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) self.applyFailureState() } } receiveValue: { [weak self] response in guard let self else { return } guard requestId == self.latestRequestId else { return } do { let jsonDecoder = JSONDecoder() let decoded = try jsonDecoder.decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { self.applySuccessState(data, isNextPage: isNextPage) } else { self.errorMessage = decoded.message ?? I18n.MainContentAll.loadFailedMessage self.isShowPopup = true } } catch { ERROR_LOG(error.localizedDescription) self.errorMessage = I18n.MainContentAll.loadFailedMessage self.isShowPopup = true } self.hasLoaded = true self.isLoading = false self.isLoadingNextPage = false } .store(in: &subscription) } private func applySuccessState(_ data: MainContentAllTabResponse, isNextPage: Bool) { response = data selectedType = data.type selectedSort = data.sort if let dayOfWeek = data.dayOfWeek { selectedDayOfWeek = dayOfWeek } page = data.page size = data.size hasNext = data.hasNext if data.type.usesAudioItems { if isNextPage { audios.append(contentsOf: data.audios) } else { audios = data.audios series = [] } } else { if isNextPage { series.append(contentsOf: data.series) } else { series = data.series audios = [] } } } private func applyFailureState() { errorMessage = I18n.MainContentAll.loadFailedMessage isShowPopup = true hasLoaded = true isLoading = false isLoadingNextPage = false } private func clearItems() { audios = [] series = [] response = nil page = 0 hasNext = false } } extension SeriesPublishedDaysOfWeek { static var currentDeviceDay: SeriesPublishedDaysOfWeek { switch Calendar.current.component(.weekday, from: Date()) { case 1: return .SUN case 2: return .MON case 3: return .TUE case 4: return .WED case 5: return .THU case 6: return .FRI case 7: return .SAT default: return .SUN } } static var allFilterCases: [SeriesPublishedDaysOfWeek] { [.MON, .TUE, .WED, .THU, .FRI, .SAT, .SUN, .RANDOM] } var shortTitle: String { switch self { case .SUN: return I18n.MainContentAll.DayOfWeek.sun case .MON: return I18n.MainContentAll.DayOfWeek.mon case .TUE: return I18n.MainContentAll.DayOfWeek.tue case .WED: return I18n.MainContentAll.DayOfWeek.wed case .THU: return I18n.MainContentAll.DayOfWeek.thu case .FRI: return I18n.MainContentAll.DayOfWeek.fri case .SAT: return I18n.MainContentAll.DayOfWeek.sat case .RANDOM: return I18n.MainContentAll.DayOfWeek.random } } }