/** * Tae Won Ha - http://taewon.de - @hataewon * See LICENSE */ import Foundation import RxSwift class Context { let stateSource: Observable let actionEmitter = Emitter() init(_ state: AppState) { let baseServerUrl = URL(string: "http://localhost:\(NetUtils.openPort())")! self.appState = state self.stateSource = self.stateSubject.asObservable() let actionSource = self.actionEmitter.observable let openQuicklyReducer = OpenQuicklyReducer() let previewReducer = PreviewReducer(baseServerUrl: baseServerUrl) let previewService = PreviewService() // For clean quit stateSource .filter { $0.quitWhenNoMainWindow && $0.mainWindows.isEmpty } .subscribe(onNext: { state in NSApp.stop(self) }) .addDisposableTo(self.disposeBag) // AppState Observable .of( actionSource .mapOmittingNil { $0 as? AppDelegate.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: AppDelegateReducer(baseServerUrl: baseServerUrl)) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? UuidAction } .map { self.appStateActionPair(for: $0) } .reduce(by: UiRootReducer()) .reduce(by: openQuicklyReducer.forMainWindow) .filter { $0.modified } .apply(to: PrefService()) .map { $0.state }, actionSource .mapOmittingNil { $0 as? FileMonitor.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: FileMonitorReducer()) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? OpenQuicklyWindow.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: openQuicklyReducer.forOpenQuicklyWindow) .filter { $0.modified } .map { $0.state } ) .merge() .subscribe(onNext: self.emitAppState) .addDisposableTo(self.disposeBag) // MainWindow.State Observable .of( actionSource .mapOmittingNil { $0 as? UuidAction } .mapOmittingNil { self.mainWindowStateActionPair(for: $0) } .reduce(by: MainWindowReducer()) .reduce(by: previewReducer.forMainWindow) .filter { $0.modified } .apply(to: previewService.forMainWindow) .apply(to: HttpServerService(port: baseServerUrl.port ?? 0)) .map { $0.state }, actionSource .mapOmittingNil { $0 as? UuidAction } .mapOmittingNil { self.mainWindowStateActionPair(for: $0) } .reduce(by: PreviewToolReducer(baseServerUrl: baseServerUrl)) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? UuidAction } .mapOmittingNil { self.mainWindowStateActionPair(for: $0) } .reduce(by: FileBrowserReducer()) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? UuidAction } .mapOmittingNil { self.mainWindowStateActionPair(for: $0) } .reduce(by: OpenedFileListReducer()) .reduce(by: previewReducer.forOpenedFileList) .filter { $0.modified } .apply(to: previewService.forOpenedFileList) .map { $0.state } ) .merge() .subscribe(onNext: self.emitAppState) .addDisposableTo(self.disposeBag) // Preferences Observable .of( actionSource .mapOmittingNil { $0 as? PrefWindow.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: PrefWindowReducer()) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? GeneralPref.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: GeneralPrefReducer()) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? AppearancePref.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: AppearancePrefReducer()) .filter { $0.modified } .map { $0.state }, actionSource .mapOmittingNil { $0 as? AdvancedPref.Action } .map { self.appStateActionPair(for: $0) } .reduce(by: AdvancedPrefReducer()) .filter { $0.modified } .map { $0.state } ) .merge() .subscribe(onNext: self.emitAppState) .addDisposableTo(self.disposeBag) #if DEBUG // actionSource.debug().subscribe().addDisposableTo(self.disposeBag) // stateSource // .filter { $0.mainWindows.values.count > 0 } // .map { Array($0.mainWindows.values)[0].preview } // .debug() // .subscribe(onNext: { state in // }) // .addDisposableTo(self.disposeBag) #endif } deinit { self.stateSubject.onCompleted() } fileprivate let stateSubject = PublishSubject() fileprivate let scheduler = SerialDispatchQueueScheduler(qos: .userInitiated) fileprivate let disposeBag = DisposeBag() fileprivate var appState: AppState fileprivate func emitAppState(_ mainWindow: UuidState) { self.appState.mainWindows[mainWindow.uuid] = mainWindow.payload self.stateSubject.onNext(self.appState) self.cleanUpAppState() } fileprivate func emitAppState(_ appState: AppState) { self.appState = appState self.stateSubject.onNext(self.appState) self.cleanUpAppState() } fileprivate func cleanUpAppState() { self.appState.mainWindows.keys.forEach { uuid in if self.appState.mainWindows[uuid]?.close == true { self.appState.mainWindows.removeValue(forKey: uuid) return } self.appState.mainWindows[uuid]?.viewToBeFocused = nil self.appState.mainWindows[uuid]?.urlsToOpen.removeAll() } } fileprivate func appStateActionPair(for action: ActionType) -> StateActionPair { return StateActionPair(state: self.appState, action: action, modified: false) } fileprivate func mainWindowStateActionPair(for action: UuidAction) -> StateActionPair, ActionType>? { guard let mainWindowState = self.appState.mainWindows[action.uuid] else { return nil } return StateActionPair(state: UuidState(uuid: action.uuid, state: mainWindowState), action: action.payload, modified: false) } } extension Observable { fileprivate func reduce(by transformer: T) -> Observable where T.Pair == Element { return transformer.reduce(self) } // If the following is used, the compiler does not finish... // fileprivate func transform(by transformers: [T]) -> Observable where T.Element == Element { // return transformers.reduce(self) { (result: Observable, transformer: T) -> Observable in // transformer.transform(result) // } // } fileprivate func apply(to service: S) -> Observable where S.Pair == Element { return self.do(onNext: service.apply) } }