// // ContentSettingsViewModel.swift // SodaLive // // Created by klaus on 10/10/24. // import Foundation import Combine final class ContentSettingsViewModel: ObservableObject { private let userRepository = UserRepository() private var subscription = Set() private let contentPreferenceSubject = PassthroughSubject() private var latestRequestToken = UUID() private var isApplyingServerState = false private var lastSyncedState: ContentPreferenceState @Published var isLoading = false @Published var errorMessage = "" @Published var isShowPopup = false @Published var isShowAdultContentAgeCheckDialog = false @Published var isAdultContentVisible: Bool @Published var adultContentPreference: ContentType init() { let isAdultContentVisible = UserDefaults.isAdultContentVisible() let contentPreference = ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? .ALL let initialState = ContentPreferenceState( isAdultContentVisible: isAdultContentVisible, contentType: isAdultContentVisible ? contentPreference : .ALL ) _isAdultContentVisible = Published(initialValue: isAdultContentVisible) _adultContentPreference = Published(initialValue: isAdultContentVisible ? contentPreference : .ALL) lastSyncedState = initialState bindContentPreference() } private func bindContentPreference() { $isAdultContentVisible .dropFirst() .removeDuplicates() .sink { [weak self] isAdultContentVisible in guard let self = self else { return } if self.isApplyingServerState { return } if !isAdultContentVisible && self.adultContentPreference != .ALL { self.adultContentPreference = .ALL } } .store(in: &subscription) Publishers.CombineLatest($isAdultContentVisible, $adultContentPreference) .map { isAdultContentVisible, adultContentPreference in ContentPreferenceState( isAdultContentVisible: isAdultContentVisible, contentType: isAdultContentVisible ? adultContentPreference : .ALL ) } .removeDuplicates() .dropFirst() .sink { [weak self] state in guard let self = self else { return } if self.isApplyingServerState { return } self.applyLocalState(state) self.contentPreferenceSubject.send(state) } .store(in: &subscription) contentPreferenceSubject .removeDuplicates() .debounce(for: .seconds(0.3), scheduler: RunLoop.main) .sink { [weak self] state in self?.updateContentPreference(state: state) } .store(in: &subscription) } private func updateContentPreference(state: ContentPreferenceState) { let request = makeUpdateContentPreferenceRequest(from: lastSyncedState, to: state) if request.isEmpty { return } let requestToken = UUID() latestRequestToken = requestToken isLoading = true userRepository .updateContentPreference( request: request ) .sink { [weak self] result in guard let self = self else { return } guard self.latestRequestToken == requestToken else { return } switch result { case .finished: DEBUG_LOG("finish") case .failure(let error): ERROR_LOG(error.localizedDescription) self.errorMessage = I18n.Common.commonError self.isShowPopup = true self.isLoading = false } } receiveValue: { [weak self] response in guard let self = self else { return } guard self.latestRequestToken == requestToken else { return } do { let decoded = try JSONDecoder().decode(ApiResponse.self, from: response.data) if let data = decoded.data, decoded.success { let serverState = ContentPreferenceState( isAdultContentVisible: data.isAdultContentVisible, contentType: data.isAdultContentVisible ? data.contentType : .ALL ) self.applyServerState(serverState) } else { self.errorMessage = decoded.message ?? I18n.Common.commonError self.isShowPopup = true } } catch { self.errorMessage = I18n.Common.commonError self.isShowPopup = true } self.isLoading = false } .store(in: &subscription) } private func applyLocalState(_ state: ContentPreferenceState) { UserDefaults.set(state.isAdultContentVisible, forKey: .isAdultContentVisible) UserDefaults.set(state.contentType.rawValue, forKey: .contentPreference) AppState.shared.isRestartApp = true } private func applyServerState(_ state: ContentPreferenceState) { isApplyingServerState = true isAdultContentVisible = state.isAdultContentVisible adultContentPreference = state.contentType applyLocalState(state) lastSyncedState = state isApplyingServerState = false } private func makeUpdateContentPreferenceRequest(from previousState: ContentPreferenceState, to currentState: ContentPreferenceState) -> UpdateContentPreferenceRequest { let isAdultContentVisible = previousState.isAdultContentVisible != currentState.isAdultContentVisible ? currentState.isAdultContentVisible : nil let contentType = previousState.contentType != currentState.contentType ? currentState.contentType : nil return UpdateContentPreferenceRequest( isAdultContentVisible: isAdultContentVisible, contentType: contentType ) } func handleAdultContentToggleTap() { if isAdultContentVisible { isAdultContentVisible = false } else { isShowAdultContentAgeCheckDialog = true } } func confirmAdultContentAgeCheck() { isShowAdultContentAgeCheckDialog = false isAdultContentVisible = true } func cancelAdultContentAgeCheck() { isShowAdultContentAgeCheckDialog = false } } private struct ContentPreferenceState: Equatable { let isAdultContentVisible: Bool let contentType: ContentType }