mirror of
https://github.com/Finb/Bark.git
synced 2025-12-08 21:36:01 +00:00
格式化代码
This commit is contained in:
parent
7be222b370
commit
13a11ebcf4
@ -6,15 +6,14 @@
|
||||
// Copyright © 2018年 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import UserNotifications
|
||||
import RealmSwift
|
||||
import IceCream
|
||||
import Material
|
||||
import RealmSwift
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
@UIApplicationMain
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate{
|
||||
|
||||
class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterDelegate {
|
||||
var window: UIWindow?
|
||||
var syncEngine: SyncEngine?
|
||||
func setupRealm() {
|
||||
@ -23,18 +22,19 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
let config = Realm.Configuration(
|
||||
fileURL: fileUrl,
|
||||
schemaVersion: 13,
|
||||
migrationBlock: { migration, oldSchemaVersion in
|
||||
migrationBlock: { _, oldSchemaVersion in
|
||||
// We haven’t migrated anything yet, so oldSchemaVersion == 0
|
||||
if (oldSchemaVersion < 1) {
|
||||
if oldSchemaVersion < 1 {
|
||||
// Nothing to do!
|
||||
// Realm will automatically detect new properties and removed properties
|
||||
// And will update the schema on disk automatically
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
// Tell Realm to use this new configuration object for the default Realm
|
||||
Realm.Configuration.defaultConfiguration = config
|
||||
|
||||
//iCloud 同步
|
||||
// iCloud 同步
|
||||
syncEngine = SyncEngine(objects: [
|
||||
SyncObject(type: Message.self)
|
||||
], databaseScope: .private)
|
||||
@ -44,54 +44,54 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
print("message count: \(realm?.objects(Message.self).count ?? 0)")
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
|
||||
//必须在应用一开始就配置,否则应用可能提前在配置之前试用了 Realm() ,则会创建两个独立数据库。
|
||||
// 必须在应用一开始就配置,否则应用可能提前在配置之前试用了 Realm() ,则会创建两个独立数据库。
|
||||
setupRealm()
|
||||
|
||||
|
||||
self.window = UIWindow(frame: UIScreen.main.bounds)
|
||||
if #available(iOS 13.0, *) {
|
||||
self.window?.overrideUserInterfaceStyle = .light
|
||||
}
|
||||
let tabBarController = StateStorageTabBarController()
|
||||
tabBarController.tabBar.tintColor = UIColor.black
|
||||
|
||||
|
||||
self.window?.backgroundColor = UIColor.black
|
||||
self.window?.rootViewController = BarkSnackbarController(
|
||||
rootViewController: tabBarController
|
||||
)
|
||||
|
||||
|
||||
tabBarController.viewControllers = [
|
||||
BarkNavigationController(rootViewController:HomeViewController(viewModel: HomeViewModel())),
|
||||
BarkNavigationController(rootViewController:MessageListViewController(viewModel: MessageListViewModel())),
|
||||
BarkNavigationController(rootViewController:MessageSettingsViewController(viewModel: MessageSettingsViewModel())),
|
||||
BarkNavigationController(rootViewController: HomeViewController(viewModel: HomeViewModel())),
|
||||
BarkNavigationController(rootViewController: MessageListViewController(viewModel: MessageListViewModel())),
|
||||
BarkNavigationController(rootViewController: MessageSettingsViewController(viewModel: MessageSettingsViewModel()))
|
||||
]
|
||||
|
||||
|
||||
let tabBarItems = [UITabBarItem(title: NSLocalizedString("service"), image: UIImage(named: "baseline_gite_black_24pt"), tag: 0),
|
||||
UITabBarItem(title: NSLocalizedString("historyMessage"), image: Icon.history, tag: 1),
|
||||
UITabBarItem(title: NSLocalizedString("settings"), image: UIImage(named: "baseline_manage_accounts_black_24pt"), tag: 2)]
|
||||
for (index , viewController) in tabBarController.viewControllers!.enumerated() {
|
||||
for (index, viewController) in tabBarController.viewControllers!.enumerated() {
|
||||
viewController.tabBarItem = tabBarItems[index]
|
||||
}
|
||||
|
||||
|
||||
self.window?.makeKeyAndVisible()
|
||||
|
||||
UNUserNotificationCenter.current().delegate = self
|
||||
UNUserNotificationCenter.current().setNotificationCategories([
|
||||
UNNotificationCategory(identifier: "myNotificationCategory", actions: [
|
||||
UNNotificationAction(identifier: "copy", title: NSLocalizedString("Copy2"), options: UNNotificationActionOptions.foreground)
|
||||
], intentIdentifiers: [], options: .customDismissAction)
|
||||
])
|
||||
], intentIdentifiers: [], options: .customDismissAction)
|
||||
])
|
||||
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
dispatch_sync_safely_main_queue {
|
||||
if settings.authorizationStatus == .authorized {
|
||||
Client.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//调整返回按钮样式
|
||||
|
||||
// 调整返回按钮样式
|
||||
let bar = UINavigationBar.appearance(whenContainedInInstancesOf: [BarkNavigationController.self])
|
||||
bar.backIndicatorImage = UIImage(named: "back")
|
||||
bar.backIndicatorTransitionMaskImage = UIImage(named: "back")
|
||||
@ -103,56 +103,57 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
func application(_ application: UIApplication, didFailToRegisterForRemoteNotificationsWithError error: Error) {
|
||||
print(error)
|
||||
}
|
||||
|
||||
|
||||
func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) {
|
||||
let deviceTokenString = deviceToken.map { String(format: "%02.2hhx", $0) }.joined()
|
||||
Settings[.deviceToken] = deviceTokenString
|
||||
|
||||
//注册设备
|
||||
|
||||
// 注册设备
|
||||
Client.shared.bindDeviceToken()
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, willPresent notification: UNNotification, withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) {
|
||||
notificatonHandler(userInfo: notification.request.content.userInfo)
|
||||
}
|
||||
|
||||
func userNotificationCenter(_ center: UNUserNotificationCenter, didReceive response: UNNotificationResponse, withCompletionHandler completionHandler: @escaping () -> Void) {
|
||||
notificatonHandler(userInfo: response.notification.request.content.userInfo)
|
||||
}
|
||||
private func notificatonHandler(userInfo:[AnyHashable:Any]){
|
||||
|
||||
|
||||
private func notificatonHandler(userInfo: [AnyHashable: Any]) {
|
||||
let navigationController = Client.shared.currentNavigationController
|
||||
func presentController(){
|
||||
let alert = (userInfo["aps"] as? [String:Any])?["alert"] as? [String:Any]
|
||||
func presentController() {
|
||||
let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any]
|
||||
let title = alert?["title"] as? String
|
||||
let body = alert?["body"] as? String
|
||||
let url:URL? = {
|
||||
let url: URL? = {
|
||||
if let url = userInfo["url"] as? String {
|
||||
return URL(string: url)
|
||||
}
|
||||
return nil
|
||||
}()
|
||||
|
||||
//URL 直接打开
|
||||
|
||||
// URL 直接打开
|
||||
if let url = url {
|
||||
if ["http","https"].contains(url.scheme?.lowercased() ?? ""){
|
||||
if ["http", "https"].contains(url.scheme?.lowercased() ?? "") {
|
||||
navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
|
||||
let alertController = UIAlertController(title: title, message: body, preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: "复制内容", style: .default, handler: { (_) in
|
||||
alertController.addAction(UIAlertAction(title: "复制内容", style: .default, handler: { _ in
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
}
|
||||
else{
|
||||
else {
|
||||
UIPasteboard.general.string = body
|
||||
}
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "更多操作", style: .default, handler: { (_) in
|
||||
alertController.addAction(UIAlertAction(title: "更多操作", style: .default, handler: { _ in
|
||||
var shareContent = ""
|
||||
if let title = title {
|
||||
shareContent += "\(title)\n"
|
||||
@ -160,15 +161,15 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
if let body = body {
|
||||
shareContent += "\(body)\n"
|
||||
}
|
||||
for (key,value) in userInfo {
|
||||
if ["aps","title","body","url"].contains((key as? String) ?? "") {
|
||||
for (key, value) in userInfo {
|
||||
if ["aps", "title", "body", "url"].contains((key as? String) ?? "") {
|
||||
continue
|
||||
}
|
||||
shareContent += "\(key): \(value) \n"
|
||||
}
|
||||
var items:[Any] = []
|
||||
var items: [Any] = []
|
||||
items.append(shareContent)
|
||||
if let url = url{
|
||||
if let url = url {
|
||||
items.append(url)
|
||||
}
|
||||
let controller = UIApplication.shared.keyWindow?.rootViewController
|
||||
@ -177,16 +178,16 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
controller?.present(activityController, animated: true, completion: nil)
|
||||
}))
|
||||
alertController.addAction(UIAlertAction(title: "取消", style: .cancel, handler: nil))
|
||||
|
||||
|
||||
navigationController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
|
||||
if let presentedController = navigationController?.presentedViewController {
|
||||
presentedController.dismiss(animated: false) {
|
||||
presentController()
|
||||
}
|
||||
}
|
||||
else{
|
||||
else {
|
||||
presentController()
|
||||
}
|
||||
}
|
||||
@ -202,7 +203,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
}
|
||||
|
||||
func applicationWillEnterForeground(_ application: UIApplication) {
|
||||
if (Client.shared.key?.count ?? 0) <= 0{
|
||||
if (Client.shared.key?.count ?? 0) <= 0 {
|
||||
Client.shared.bindDeviceToken()
|
||||
}
|
||||
}
|
||||
@ -214,7 +215,4 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UNUserNotificationCenterD
|
||||
func applicationWillTerminate(_ application: UIApplication) {
|
||||
// Called when the application is about to terminate. Save data if appropriate. See also applicationDidEnterBackground:.
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -9,7 +9,6 @@
|
||||
import XCTest
|
||||
|
||||
class BarkTests: XCTestCase {
|
||||
|
||||
override func setUpWithError() throws {
|
||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||
}
|
||||
@ -29,5 +28,4 @@ class BarkTests: XCTestCase {
|
||||
// Put the code you want to measure the time of here.
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -10,17 +10,17 @@ import UIKit
|
||||
|
||||
class ArchiveSettingManager: NSObject {
|
||||
static let shared = ArchiveSettingManager()
|
||||
let defaults = UserDefaults.init(suiteName: "group.bark")
|
||||
let defaults = UserDefaults(suiteName: "group.bark")
|
||||
var isArchive: Bool {
|
||||
get {
|
||||
return defaults?.value(forKey: "isArchive") as? Bool ?? true
|
||||
|
||||
return defaults?.value(forKey: "isArchive") as? Bool ?? true
|
||||
}
|
||||
set{
|
||||
set {
|
||||
defaults?.set(newValue, forKey: "isArchive")
|
||||
}
|
||||
}
|
||||
private override init(){
|
||||
|
||||
override private init() {
|
||||
super.init()
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,10 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import DefaultsKit
|
||||
import UIKit
|
||||
|
||||
enum BarkSettingKey:String {
|
||||
enum BarkSettingKey: String {
|
||||
/// 存放key
|
||||
case key = "me.fin.bark.key"
|
||||
case servers = "me.fin.bark.servers"
|
||||
@ -20,13 +20,11 @@ enum BarkSettingKey:String {
|
||||
case selectedViewControllerIndex = "me.fin.bark.selectedViewControllerIndex"
|
||||
}
|
||||
|
||||
class BarkSettings{
|
||||
class BarkSettings {
|
||||
static let shared = BarkSettings()
|
||||
private init(){
|
||||
|
||||
}
|
||||
private init() {}
|
||||
|
||||
subscript(key:String) -> String? {
|
||||
subscript(key: String) -> String? {
|
||||
get {
|
||||
let storeKey = Key<String>(key)
|
||||
return Defaults.shared.get(for: storeKey)
|
||||
@ -42,7 +40,7 @@ class BarkSettings{
|
||||
}
|
||||
}
|
||||
|
||||
subscript(key:BarkSettingKey) -> String? {
|
||||
subscript(key: BarkSettingKey) -> String? {
|
||||
get {
|
||||
return self[key.rawValue]
|
||||
}
|
||||
@ -51,7 +49,7 @@ class BarkSettings{
|
||||
}
|
||||
}
|
||||
|
||||
subscript<T : Codable>(key:String) -> T? {
|
||||
subscript<T: Codable>(key: String) -> T? {
|
||||
get {
|
||||
let storeKey = Key<T>(key)
|
||||
return Defaults.shared.get(for: storeKey)
|
||||
@ -66,7 +64,8 @@ class BarkSettings{
|
||||
}
|
||||
}
|
||||
}
|
||||
subscript<T : Codable>(key:BarkSettingKey) -> T? {
|
||||
|
||||
subscript<T: Codable>(key: BarkSettingKey) -> T? {
|
||||
get {
|
||||
return self[key.rawValue]
|
||||
}
|
||||
|
||||
@ -6,31 +6,29 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
|
||||
class Client: NSObject {
|
||||
static let shared = Client()
|
||||
private override init() {
|
||||
override private init() {
|
||||
super.init()
|
||||
}
|
||||
var currentNavigationController:UINavigationController? {
|
||||
get {
|
||||
let controller = UIApplication.shared.delegate?.window??.rootViewController as? BarkSnackbarController
|
||||
let nav = (controller?.rootViewController as? UITabBarController)?.selectedViewController as? UINavigationController
|
||||
return nav
|
||||
}
|
||||
|
||||
var currentNavigationController: UINavigationController? {
|
||||
let controller = UIApplication.shared.delegate?.window??.rootViewController as? BarkSnackbarController
|
||||
let nav = (controller?.rootViewController as? UITabBarController)?.selectedViewController as? UINavigationController
|
||||
return nav
|
||||
}
|
||||
var currentTabBarController:StateStorageTabBarController? {
|
||||
get {
|
||||
let controller = UIApplication.shared.delegate?.window??.rootViewController as? BarkSnackbarController
|
||||
return controller?.rootViewController as? StateStorageTabBarController
|
||||
}
|
||||
|
||||
var currentTabBarController: StateStorageTabBarController? {
|
||||
let controller = UIApplication.shared.delegate?.window??.rootViewController as? BarkSnackbarController
|
||||
return controller?.rootViewController as? StateStorageTabBarController
|
||||
}
|
||||
|
||||
let appVersion:String = {
|
||||
let appVersion: String = {
|
||||
var version = "0.0.0"
|
||||
if let infoDict = Bundle.main.infoDictionary {
|
||||
if let appVersion = infoDict["CFBundleVersion"] as? String {
|
||||
@ -40,10 +38,10 @@ class Client: NSObject {
|
||||
return version
|
||||
}()
|
||||
|
||||
private var _key:String?
|
||||
var key:String? {
|
||||
private var _key: String?
|
||||
var key: String? {
|
||||
get {
|
||||
if _key == nil, let aKey = Settings[.key]{
|
||||
if _key == nil, let aKey = Settings[.key] {
|
||||
_key = aKey
|
||||
}
|
||||
return _key
|
||||
@ -62,24 +60,24 @@ class Client: NSObject {
|
||||
|
||||
var state = BehaviorRelay<ClienState>(value: .ok)
|
||||
|
||||
var dispose:Disposable?
|
||||
func bindDeviceToken(){
|
||||
if let token = Settings[.deviceToken] , token.count > 0{
|
||||
var dispose: Disposable?
|
||||
func bindDeviceToken() {
|
||||
if let token = Settings[.deviceToken], token.count > 0 {
|
||||
dispose?.dispose()
|
||||
|
||||
dispose = BarkApi.provider
|
||||
.request(.register(
|
||||
key: key,
|
||||
devicetoken: token))
|
||||
key: key,
|
||||
devicetoken: token))
|
||||
.filterResponseError()
|
||||
.map { (json) -> ClienState in
|
||||
.map { json -> ClienState in
|
||||
switch json {
|
||||
case .success(let json):
|
||||
if let key = json["data","key"].rawString() {
|
||||
if let key = json["data", "key"].rawString() {
|
||||
Client.shared.key = key
|
||||
return .ok
|
||||
}
|
||||
else{
|
||||
else {
|
||||
return .serverError
|
||||
}
|
||||
case .failure:
|
||||
@ -92,13 +90,13 @@ class Client: NSObject {
|
||||
|
||||
func registerForRemoteNotifications() {
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert , .sound , .badge], completionHandler: {(_ granted: Bool, _ error: Error?) -> Void in
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { (_ granted: Bool, _: Error?) -> Void in
|
||||
if granted {
|
||||
dispatch_sync_safely_main_queue {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
}
|
||||
}
|
||||
else{
|
||||
else {
|
||||
print("没有打开推送")
|
||||
}
|
||||
})
|
||||
|
||||
@ -9,23 +9,22 @@
|
||||
import UIKit
|
||||
|
||||
extension Date {
|
||||
func formatString(format:String) -> String {
|
||||
func formatString(format: String) -> String {
|
||||
let formatter = DateFormatter()
|
||||
formatter.dateFormat = format
|
||||
return formatter.string(for: self) ?? ""
|
||||
}
|
||||
|
||||
|
||||
func agoFormatString() -> String {
|
||||
|
||||
let clendar = NSCalendar(calendarIdentifier: .gregorian)
|
||||
let cps = clendar?.components([ .hour, .minute, .second, .day, .month, .year], from: self, to: Date(), options: .wrapComponents)
|
||||
|
||||
let cps = clendar?.components([.hour, .minute, .second, .day, .month, .year], from: self, to: Date(), options: .wrapComponents)
|
||||
|
||||
let year = cps!.year!
|
||||
let month = cps!.month!
|
||||
let day = cps!.day!
|
||||
let hour = cps!.hour!
|
||||
let minute = cps!.minute!
|
||||
|
||||
|
||||
if year > 0 || month > 0 || day > 0 || hour > 12 {
|
||||
return formatString(format: "yyyy-MM-dd HH:mm")
|
||||
}
|
||||
@ -44,20 +43,24 @@ extension Date {
|
||||
|
||||
extension Date {
|
||||
static var yesterday: Date { return Date().dayBefore }
|
||||
static var tomorrow: Date { return Date().dayAfter }
|
||||
static var lastHour: Date { return Calendar.current.date(byAdding: .hour, value: -1, to: Date())! }
|
||||
static var tomorrow: Date { return Date().dayAfter }
|
||||
static var lastHour: Date { return Calendar.current.date(byAdding: .hour, value: -1, to: Date())! }
|
||||
var dayBefore: Date {
|
||||
return Calendar.current.date(byAdding: .day, value: -1, to: noon)!
|
||||
}
|
||||
|
||||
var dayAfter: Date {
|
||||
return Calendar.current.date(byAdding: .day, value: 1, to: noon)!
|
||||
}
|
||||
|
||||
var noon: Date {
|
||||
return Calendar.current.date(bySettingHour: 0, minute: 0, second: 0, of: self)!
|
||||
}
|
||||
|
||||
var month: Int {
|
||||
return Calendar.current.component(.month, from: self)
|
||||
return Calendar.current.component(.month, from: self)
|
||||
}
|
||||
|
||||
var isLastDayOfMonth: Bool {
|
||||
return dayAfter.month != month
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//
|
||||
//
|
||||
// Defines.swift
|
||||
// Bark
|
||||
//
|
||||
@ -9,7 +9,7 @@
|
||||
import UIKit
|
||||
|
||||
/// 将代码安全的运行在主线程
|
||||
func dispatch_sync_safely_main_queue(_ block: ()->()) {
|
||||
func dispatch_sync_safely_main_queue(_ block: () -> ()) {
|
||||
if Thread.isMainThread {
|
||||
block()
|
||||
} else {
|
||||
@ -20,29 +20,28 @@ func dispatch_sync_safely_main_queue(_ block: ()->()) {
|
||||
}
|
||||
|
||||
extension UIViewController {
|
||||
func showSnackbar(text:String) {
|
||||
func showSnackbar(text: String) {
|
||||
self.snackbarController?.snackbar.text = text
|
||||
self.snackbarController?.animate(snackbar: .visible)
|
||||
self.snackbarController?.animate(snackbar: .hidden, delay: 3)
|
||||
}
|
||||
}
|
||||
|
||||
func NSLocalizedString( _ key:String ) -> String {
|
||||
func NSLocalizedString(_ key: String) -> String {
|
||||
return NSLocalizedString(key, comment: "")
|
||||
}
|
||||
|
||||
let kNavigationHeight: CGFloat = {
|
||||
return kSafeAreaInsets.top + 44
|
||||
kSafeAreaInsets.top + 44
|
||||
}()
|
||||
|
||||
let kSafeAreaInsets:UIEdgeInsets = {
|
||||
if #available(iOS 12.0, *){
|
||||
let kSafeAreaInsets: UIEdgeInsets = {
|
||||
if #available(iOS 12.0, *) {
|
||||
return UIWindow().safeAreaInsets
|
||||
}
|
||||
else if #available(iOS 11.0, *){
|
||||
} else if #available(iOS 11.0, *) {
|
||||
let inset = UIWindow().safeAreaInsets
|
||||
if inset.top > 0 { return inset}
|
||||
//iOS 11下,不是全面屏的手机 safeAreaInsets.top 是 0,与iOS12 不一致,这里强行让他们保持一致,方便开发
|
||||
if inset.top > 0 { return inset }
|
||||
// iOS 11下,不是全面屏的手机 safeAreaInsets.top 是 0,与iOS12 不一致,这里强行让他们保持一致,方便开发
|
||||
}
|
||||
return UIEdgeInsets(top: 20, left: 0, bottom: 0, right: 0)
|
||||
}()
|
||||
|
||||
@ -7,14 +7,13 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import MJRefresh
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
import MJRefresh
|
||||
|
||||
extension Reactive where Base : MJRefreshComponent {
|
||||
extension Reactive where Base: MJRefreshComponent {
|
||||
var refresh: ControlEvent<Void> {
|
||||
|
||||
let source = Observable<Void>.create {[weak control = self.base] (observer) -> Disposable in
|
||||
let source = Observable<Void>.create { [weak control = self.base] observer -> Disposable in
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
guard let control = control else {
|
||||
observer.onCompleted()
|
||||
@ -27,10 +26,8 @@ extension Reactive where Base : MJRefreshComponent {
|
||||
}
|
||||
return ControlEvent(events: source)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
enum MJRefreshAction {
|
||||
/// 不做任何事情
|
||||
case none
|
||||
@ -48,16 +45,14 @@ enum MJRefreshAction {
|
||||
case resetNomoreData
|
||||
}
|
||||
|
||||
extension Reactive where Base:UIScrollView {
|
||||
|
||||
extension Reactive where Base: UIScrollView {
|
||||
/// 执行的操作类型
|
||||
var refreshAction:Binder<MJRefreshAction> {
|
||||
|
||||
return Binder(base) { (target, action) in
|
||||
|
||||
switch action{
|
||||
var refreshAction: Binder<MJRefreshAction> {
|
||||
return Binder(base) { target, action in
|
||||
|
||||
switch action {
|
||||
case .begainRefresh:
|
||||
//下拉刷新使用 UIRefreshControl
|
||||
// 下拉刷新使用 UIRefreshControl
|
||||
if let control = target.refreshControl {
|
||||
control.beginRefreshing()
|
||||
}
|
||||
@ -66,26 +61,24 @@ extension Reactive where Base:UIScrollView {
|
||||
control.endRefreshing()
|
||||
}
|
||||
case .begainLoadmore:
|
||||
if let footer = target.mj_footer {
|
||||
if let footer = target.mj_footer {
|
||||
footer.beginRefreshing()
|
||||
}
|
||||
case .endLoadmore:
|
||||
if let footer = target.mj_footer {
|
||||
if let footer = target.mj_footer {
|
||||
footer.endRefreshing()
|
||||
}
|
||||
case .showNomoreData:
|
||||
if let footer = target.mj_footer {
|
||||
if let footer = target.mj_footer {
|
||||
footer.endRefreshingWithNoMoreData()
|
||||
}
|
||||
case .resetNomoreData:
|
||||
if let footer = target.mj_footer {
|
||||
if let footer = target.mj_footer {
|
||||
footer.resetNoMoreData()
|
||||
}
|
||||
break
|
||||
case .none:
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,21 +9,22 @@
|
||||
import UIKit
|
||||
|
||||
enum BarkApi {
|
||||
case ping(baseURL:String?)
|
||||
case register(key:String? , devicetoken:String) //注册设备
|
||||
case ping(baseURL: String?)
|
||||
case register(key: String?, devicetoken: String) // 注册设备
|
||||
}
|
||||
|
||||
extension BarkApi: BarkTargetType {
|
||||
var baseURL: URL {
|
||||
if case let .ping(urlStr) = self, let url = URL(string: urlStr ?? "") {
|
||||
if case let .ping(urlStr) = self, let url = URL(string: urlStr ?? "") {
|
||||
return url
|
||||
}
|
||||
return URL(string: ServerManager.shared.currentAddress)!
|
||||
}
|
||||
var parameters: [String : Any]? {
|
||||
|
||||
var parameters: [String: Any]? {
|
||||
switch self {
|
||||
case let .register(key, devicetoken):
|
||||
var params = ["devicetoken":devicetoken]
|
||||
var params = ["devicetoken": devicetoken]
|
||||
if let key = key {
|
||||
params["key"] = key
|
||||
}
|
||||
@ -32,7 +33,7 @@ extension BarkApi: BarkTargetType {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .ping:
|
||||
@ -41,6 +42,4 @@ extension BarkApi: BarkTargetType {
|
||||
return "/register"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@ -6,21 +6,22 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Moya
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
//保存全局Providers
|
||||
fileprivate var retainProviders:[String: Any] = [:]
|
||||
// 保存全局Providers
|
||||
private var retainProviders: [String: Any] = [:]
|
||||
|
||||
protocol BarkTargetType: TargetType {
|
||||
var parameters: [String: Any]? { get }
|
||||
}
|
||||
|
||||
extension BarkTargetType {
|
||||
var headers: [String : String]? {
|
||||
var headers: [String: String]? {
|
||||
return nil
|
||||
}
|
||||
|
||||
var baseURL: URL {
|
||||
return URL(string: ServerManager.shared.currentAddress)!
|
||||
}
|
||||
@ -42,21 +43,19 @@ extension BarkTargetType {
|
||||
}
|
||||
|
||||
var requestTaskWithParameters: Task {
|
||||
get {
|
||||
//默认参数
|
||||
var defaultParameters:[String:Any] = [:]
|
||||
//协议参数
|
||||
if let parameters = self.parameters {
|
||||
for (key, value) in parameters {
|
||||
defaultParameters[key] = value
|
||||
}
|
||||
// 默认参数
|
||||
var defaultParameters: [String: Any] = [:]
|
||||
// 协议参数
|
||||
if let parameters = self.parameters {
|
||||
for (key, value) in parameters {
|
||||
defaultParameters[key] = value
|
||||
}
|
||||
return Task.requestParameters(parameters: defaultParameters, encoding: parameterEncoding)
|
||||
}
|
||||
return Task.requestParameters(parameters: defaultParameters, encoding: parameterEncoding)
|
||||
}
|
||||
|
||||
static var networkActivityPlugin: PluginType {
|
||||
return NetworkActivityPlugin { (change, type) in
|
||||
return NetworkActivityPlugin { change, _ in
|
||||
switch change {
|
||||
case .began:
|
||||
dispatch_sync_safely_main_queue {
|
||||
@ -71,9 +70,9 @@ extension BarkTargetType {
|
||||
}
|
||||
|
||||
/// 实现此协议的类,将自动获得用该类实例化的 provider 对象
|
||||
static var provider: RxSwift.Reactive< MoyaProvider<Self> > {
|
||||
static var provider: RxSwift.Reactive<MoyaProvider<Self>> {
|
||||
let key = "\(Self.self)"
|
||||
if let provider = retainProviders[key] as? RxSwift.Reactive< MoyaProvider<Self> > {
|
||||
if let provider = retainProviders[key] as? RxSwift.Reactive<MoyaProvider<Self>> {
|
||||
return provider
|
||||
}
|
||||
let provider = Self.weakProvider
|
||||
@ -82,18 +81,18 @@ extension BarkTargetType {
|
||||
}
|
||||
|
||||
/// 不被全局持有的 Provider ,使用时,需要持有它,否则将立即释放,请求随即终止
|
||||
static var weakProvider: RxSwift.Reactive< MoyaProvider<Self> > {
|
||||
var plugins:[PluginType] = [networkActivityPlugin]
|
||||
static var weakProvider: RxSwift.Reactive<MoyaProvider<Self>> {
|
||||
var plugins: [PluginType] = [networkActivityPlugin]
|
||||
#if DEBUG
|
||||
plugins.append(LogPlugin())
|
||||
#endif
|
||||
let provider = MoyaProvider<Self>(plugins:plugins)
|
||||
let provider = MoyaProvider<Self>(plugins: plugins)
|
||||
return provider.rx
|
||||
}
|
||||
}
|
||||
|
||||
extension RxSwift.Reactive where Base: MoyaProviderType {
|
||||
public func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Observable<Response> {
|
||||
public extension RxSwift.Reactive where Base: MoyaProviderType {
|
||||
func request(_ token: Base.Target, callbackQueue: DispatchQueue? = nil) -> Observable<Response> {
|
||||
return Single.create { [weak base] single in
|
||||
let cancellableToken = base?.request(token, callbackQueue: callbackQueue, progress: nil) { result in
|
||||
switch result {
|
||||
@ -107,11 +106,11 @@ extension RxSwift.Reactive where Base: MoyaProviderType {
|
||||
return Disposables.create {
|
||||
cancellableToken?.cancel()
|
||||
}
|
||||
}.asObservable()
|
||||
}.asObservable()
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate class LogPlugin: PluginType{
|
||||
private class LogPlugin: PluginType {
|
||||
func willSend(_ request: RequestType, target: TargetType) {
|
||||
print("\n-------------------\n准备请求: \(target.path)")
|
||||
print("请求方式: \(target.method.rawValue)")
|
||||
@ -119,8 +118,8 @@ fileprivate class LogPlugin: PluginType{
|
||||
print(params)
|
||||
}
|
||||
print("\n")
|
||||
|
||||
}
|
||||
|
||||
func didReceive(_ result: Result<Response, MoyaError>, target: TargetType) {
|
||||
print("\n-------------------\n请求结束: \(target.path)")
|
||||
if let data = try? result.get().data, let resutl = String(data: data, encoding: String.Encoding.utf8) {
|
||||
|
||||
@ -8,13 +8,13 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import ObjectMapper
|
||||
import SwiftyJSON
|
||||
import Moya
|
||||
import ObjectMapper
|
||||
import RxSwift
|
||||
import SwiftyJSON
|
||||
import UIKit
|
||||
|
||||
public enum ApiError : Swift.Error {
|
||||
public enum ApiError: Swift.Error {
|
||||
case Error(info: String)
|
||||
case AccountBanned(info: String)
|
||||
}
|
||||
@ -25,9 +25,9 @@ extension Swift.Error {
|
||||
return self.localizedDescription
|
||||
}
|
||||
switch err {
|
||||
case let .Error(info):
|
||||
case .Error(let info):
|
||||
return info
|
||||
case let .AccountBanned(info):
|
||||
case .AccountBanned(let info):
|
||||
return info
|
||||
}
|
||||
}
|
||||
@ -35,23 +35,23 @@ extension Swift.Error {
|
||||
|
||||
extension Observable where Element: Moya.Response {
|
||||
/// 过滤 HTTP 错误,例如超时,请求失败等
|
||||
func filterHttpError() -> Observable<Result<Element,ApiError>> {
|
||||
func filterHttpError() -> Observable<Result<Element, ApiError>> {
|
||||
return
|
||||
catchErrorJustReturn(Element(statusCode: 599, data: Data()))
|
||||
.map { (response) -> Result<Element,ApiError> in
|
||||
if (200...209) ~= response.statusCode {
|
||||
return .success(response)
|
||||
.map { response -> Result<Element, ApiError> in
|
||||
if (200 ... 209) ~= response.statusCode {
|
||||
return .success(response)
|
||||
}
|
||||
else {
|
||||
return .failure(ApiError.Error(info: "网络错误"))
|
||||
}
|
||||
}
|
||||
else{
|
||||
return .failure(ApiError.Error(info: "网络错误"))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 过滤逻辑错误,例如协议里返回 错误CODE
|
||||
func filterResponseError() -> Observable<Result<JSON,ApiError>> {
|
||||
func filterResponseError() -> Observable<Result<JSON, ApiError>> {
|
||||
return filterHttpError()
|
||||
.map{ response -> Result<JSON,ApiError> in
|
||||
.map { response -> Result<JSON, ApiError> in
|
||||
switch response {
|
||||
case .success(let element):
|
||||
do {
|
||||
@ -59,11 +59,12 @@ extension Observable where Element: Moya.Response {
|
||||
|
||||
if let codeStr = json["code"].rawString(),
|
||||
let code = Int(codeStr),
|
||||
code == 200{
|
||||
code == 200
|
||||
{
|
||||
return .success(json)
|
||||
}
|
||||
else{
|
||||
var msg:String = ""
|
||||
else {
|
||||
var msg: String = ""
|
||||
if json["message"].exists() {
|
||||
msg = json["message"].rawString()!
|
||||
}
|
||||
@ -73,7 +74,7 @@ extension Observable where Element: Moya.Response {
|
||||
catch {
|
||||
return .failure(ApiError.Error(info: error.rawString()))
|
||||
}
|
||||
case .failure(let error) :
|
||||
case .failure(let error):
|
||||
return .failure(ApiError.Error(info: error.rawString()))
|
||||
}
|
||||
}
|
||||
@ -84,18 +85,18 @@ extension Observable where Element: Moya.Response {
|
||||
/// - Parameters:
|
||||
/// - typeName: 要转换的Model Class
|
||||
/// - dataPath: 从哪个节点开始转换,例如 ["data","links"]
|
||||
func mapResponseToObj<T: Mappable>(_ typeName: T.Type , dataPath:[String] = ["data"] ) -> Observable<Result<T,ApiError>> {
|
||||
return filterResponseError().map{ json in
|
||||
func mapResponseToObj<T: Mappable>(_ typeName: T.Type, dataPath: [String] = ["data"]) -> Observable<Result<T, ApiError>> {
|
||||
return filterResponseError().map { json in
|
||||
switch json {
|
||||
case .success(let json):
|
||||
var rootJson = json
|
||||
if dataPath.count > 0{
|
||||
if dataPath.count > 0 {
|
||||
rootJson = rootJson[dataPath]
|
||||
}
|
||||
if let model: T = self.resultFromJSON(json: rootJson) {
|
||||
if let model: T = self.resultFromJSON(json: rootJson) {
|
||||
return .success(model)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
return .failure(ApiError.Error(info: "json 转换失败"))
|
||||
}
|
||||
case .failure(let error):
|
||||
@ -105,24 +106,24 @@ extension Observable where Element: Moya.Response {
|
||||
}
|
||||
|
||||
/// 将Response 转换成 JSON Model Array
|
||||
func mapResponseToObjArray<T: Mappable>(_ type: T.Type, dataPath:[String] = ["data"] ) -> Observable<Result<[T],ApiError>> {
|
||||
return filterResponseError().map{ json in
|
||||
func mapResponseToObjArray<T: Mappable>(_ type: T.Type, dataPath: [String] = ["data"]) -> Observable<Result<[T], ApiError>> {
|
||||
return filterResponseError().map { json in
|
||||
switch json {
|
||||
case .success(let json):
|
||||
var rootJson = json;
|
||||
if dataPath.count > 0{
|
||||
var rootJson = json
|
||||
if dataPath.count > 0 {
|
||||
rootJson = rootJson[dataPath]
|
||||
}
|
||||
var result = [T]()
|
||||
guard let jsonArray = rootJson.array else{
|
||||
guard let jsonArray = rootJson.array else {
|
||||
return .failure(ApiError.Error(info: "Root Json 不是 Array"))
|
||||
}
|
||||
|
||||
for json in jsonArray{
|
||||
for json in jsonArray {
|
||||
if let jsonModel: T = self.resultFromJSON(json: json) {
|
||||
result.append(jsonModel)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
return .failure(ApiError.Error(info: "json 转换失败"))
|
||||
}
|
||||
}
|
||||
@ -131,15 +132,15 @@ extension Observable where Element: Moya.Response {
|
||||
case .failure(let error):
|
||||
return .failure(error)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
private func resultFromJSON<T: Mappable>(jsonString:String) -> T? {
|
||||
private func resultFromJSON<T: Mappable>(jsonString: String) -> T? {
|
||||
return T(JSONString: jsonString)
|
||||
}
|
||||
private func resultFromJSON<T: Mappable>(json:JSON) -> T? {
|
||||
if let str = json.rawString(){
|
||||
|
||||
private func resultFromJSON<T: Mappable>(json: JSON) -> T? {
|
||||
if let str = json.rawString() {
|
||||
return resultFromJSON(jsonString: str)
|
||||
}
|
||||
return nil
|
||||
|
||||
@ -5,8 +5,8 @@
|
||||
// Created by Krunoslav Zaher on 12/6/15.
|
||||
// Copyright © 2015 Krunoslav Zaher. All rights reserved.
|
||||
//
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
#if os(iOS)
|
||||
import UIKit
|
||||
#elseif os(macOS)
|
||||
@ -14,7 +14,7 @@ import AppKit
|
||||
#endif
|
||||
|
||||
// Two way binding operator between control property and relay, that's all it takes.
|
||||
infix operator <-> : DefaultPrecedence
|
||||
infix operator <->: DefaultPrecedence
|
||||
|
||||
#if os(iOS)
|
||||
func nonMarkedText(_ textInput: UITextInput) -> String? {
|
||||
@ -22,8 +22,9 @@ func nonMarkedText(_ textInput: UITextInput) -> String? {
|
||||
let end = textInput.endOfDocument
|
||||
|
||||
guard let rangeAll = textInput.textRange(from: start, to: end),
|
||||
let text = textInput.text(in: rangeAll) else {
|
||||
return nil
|
||||
let text = textInput.text(in: rangeAll)
|
||||
else {
|
||||
return nil
|
||||
}
|
||||
|
||||
guard let markedTextRange = textInput.markedTextRange else {
|
||||
@ -31,7 +32,8 @@ func nonMarkedText(_ textInput: UITextInput) -> String? {
|
||||
}
|
||||
|
||||
guard let startRange = textInput.textRange(from: start, to: markedTextRange.start),
|
||||
let endRange = textInput.textRange(from: markedTextRange.end, to: end) else {
|
||||
let endRange = textInput.textRange(from: markedTextRange.end, to: end)
|
||||
else {
|
||||
return text
|
||||
}
|
||||
|
||||
@ -42,7 +44,7 @@ func <-> <Base>(textInput: TextInput<Base>, relay: BehaviorRelay<String>) -> Dis
|
||||
let bindToUIDisposable = relay.bind(to: textInput.text)
|
||||
|
||||
let bindToRelay = textInput.text
|
||||
.subscribe(onNext: { [weak base = textInput.base] n in
|
||||
.subscribe(onNext: { [weak base = textInput.base] _ in
|
||||
guard let base = base else {
|
||||
return
|
||||
}
|
||||
@ -53,7 +55,7 @@ func <-> <Base>(textInput: TextInput<Base>, relay: BehaviorRelay<String>) -> Dis
|
||||
In some cases `textInput.textRangeFromPosition(start, toPosition: end)` will return nil even though the underlying
|
||||
value is not nil. This appears to be an Apple bug. If it's not, and we are doing something wrong, please let us know.
|
||||
The can be reproed easily if replace bottom code with
|
||||
|
||||
|
||||
if nonMarkedTextValue != relay.value {
|
||||
relay.accept(nonMarkedTextValue ?? "")
|
||||
}
|
||||
@ -62,7 +64,7 @@ func <-> <Base>(textInput: TextInput<Base>, relay: BehaviorRelay<String>) -> Dis
|
||||
if let nonMarkedTextValue = nonMarkedTextValue, nonMarkedTextValue != relay.value {
|
||||
relay.accept(nonMarkedTextValue)
|
||||
}
|
||||
}, onCompleted: {
|
||||
}, onCompleted: {
|
||||
bindToUIDisposable.dispose()
|
||||
})
|
||||
|
||||
@ -77,7 +79,7 @@ func <-> <T>(property: ControlProperty<T>, relay: BehaviorRelay<T>) -> Disposabl
|
||||
"That will usually work ok, but for some languages that use IME, that simplistic method could cause unexpected issues because it will return intermediate results while text is being inputed.\n" +
|
||||
"REMEDY: Just use `textField <-> relay` instead of `textField.rx.text <-> relay`.\n" +
|
||||
"Find out more here: https://github.com/ReactiveX/RxSwift/issues/649\n"
|
||||
)
|
||||
)
|
||||
#endif
|
||||
}
|
||||
|
||||
@ -85,7 +87,7 @@ func <-> <T>(property: ControlProperty<T>, relay: BehaviorRelay<T>) -> Disposabl
|
||||
let bindToRelay = property
|
||||
.subscribe(onNext: { n in
|
||||
relay.accept(n)
|
||||
}, onCompleted: {
|
||||
}, onCompleted: {
|
||||
bindToUIDisposable.dispose()
|
||||
})
|
||||
|
||||
|
||||
@ -6,13 +6,13 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
private var prepareForReuseBag: Int8 = 0
|
||||
|
||||
@objc public protocol Reusable : class {
|
||||
@objc public protocol Reusable: class {
|
||||
func prepareForReuse()
|
||||
}
|
||||
|
||||
@ -24,17 +24,17 @@ extension Reactive where Base: Reusable {
|
||||
var prepareForReuse: Observable<Void> {
|
||||
return Observable.of(sentMessage(#selector(Base.prepareForReuse)).map { _ in }, deallocated).merge()
|
||||
}
|
||||
|
||||
|
||||
var reuseBag: DisposeBag {
|
||||
MainScheduler.ensureExecutingOnScheduler()
|
||||
|
||||
|
||||
if let bag = objc_getAssociatedObject(base, &prepareForReuseBag) as? DisposeBag {
|
||||
return bag
|
||||
}
|
||||
|
||||
|
||||
let bag = DisposeBag()
|
||||
objc_setAssociatedObject(base, &prepareForReuseBag, bag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
|
||||
|
||||
|
||||
_ = sentMessage(#selector(Base.prepareForReuse))
|
||||
.subscribe(onNext: { [weak base] _ in
|
||||
guard let strongBase = base else {
|
||||
@ -43,7 +43,7 @@ extension Reactive where Base: Reusable {
|
||||
let newBag = DisposeBag()
|
||||
objc_setAssociatedObject(strongBase, &prepareForReuseBag, newBag, objc_AssociationPolicy.OBJC_ASSOCIATION_RETAIN)
|
||||
})
|
||||
|
||||
|
||||
return bag
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,39 +12,41 @@ let defaultServer = "https://api.day.app"
|
||||
|
||||
class ServerManager: NSObject {
|
||||
static let shared = ServerManager()
|
||||
private override init() {
|
||||
if let servers:Set<String> = Settings[.servers] {
|
||||
override private init() {
|
||||
if let servers: Set<String> = Settings[.servers] {
|
||||
self.servers = servers
|
||||
}
|
||||
|
||||
|
||||
if let address = Settings[.currentServer] {
|
||||
self.currentAddress = address
|
||||
}
|
||||
else{
|
||||
else {
|
||||
self.currentAddress = self.servers.first ?? defaultServer
|
||||
}
|
||||
super.init()
|
||||
}
|
||||
|
||||
var servers:Set<String> = [defaultServer]
|
||||
var currentAddress:String {
|
||||
didSet{
|
||||
|
||||
var servers: Set<String> = [defaultServer]
|
||||
var currentAddress: String {
|
||||
didSet {
|
||||
Settings[.currentServer] = currentAddress
|
||||
}
|
||||
}
|
||||
|
||||
func addServer(server:String){
|
||||
|
||||
func addServer(server: String) {
|
||||
self.servers.insert(server)
|
||||
Settings[.servers] = self.servers
|
||||
}
|
||||
func removeServer(server:String){
|
||||
|
||||
func removeServer(server: String) {
|
||||
self.servers.remove(server)
|
||||
if self.servers.count <= 0 {
|
||||
self.servers.insert(defaultServer)
|
||||
}
|
||||
Settings[.servers] = self.servers
|
||||
}
|
||||
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@ -9,14 +9,14 @@
|
||||
import UIKit
|
||||
|
||||
extension String {
|
||||
//将原始的url编码为合法的url
|
||||
// 将原始的url编码为合法的url
|
||||
func urlEncoded() -> String {
|
||||
let encodeUrlString = self.addingPercentEncoding(withAllowedCharacters:
|
||||
.urlQueryAllowed)
|
||||
return encodeUrlString ?? ""
|
||||
}
|
||||
|
||||
//将编码后的url转换回原始的url
|
||||
|
||||
// 将编码后的url转换回原始的url
|
||||
func urlDecoded() -> String {
|
||||
return self.removingPercentEncoding ?? ""
|
||||
}
|
||||
|
||||
@ -9,20 +9,21 @@
|
||||
import UIKit
|
||||
|
||||
extension UIColor {
|
||||
convenience public init(r255:CGFloat, g255:CGFloat, b255:CGFloat, a255:CGFloat = 255) {
|
||||
public convenience init(r255: CGFloat, g255: CGFloat, b255: CGFloat, a255: CGFloat = 255) {
|
||||
self.init(red: r255/255, green: g255/255, blue: b255/255, alpha: a255/255)
|
||||
}
|
||||
class func image(color:UIColor, size:CGSize = CGSize(width: 1, height: 1)) -> UIImage{
|
||||
|
||||
class func image(color: UIColor, size: CGSize = CGSize(width: 1, height: 1)) -> UIImage {
|
||||
UIGraphicsBeginImageContext(size)
|
||||
let context = UIGraphicsGetCurrentContext()
|
||||
context?.setFillColor(color.cgColor)
|
||||
context?.fill(CGRect(origin: CGPoint.zero, size: size))
|
||||
|
||||
|
||||
let image = UIGraphicsGetImageFromCurrentImageContext()
|
||||
UIGraphicsEndImageContext()
|
||||
return image! //context应该不会没get到吧~ 所以直接强解了
|
||||
return image! // context应该不会没get到吧~ 所以直接强解了
|
||||
}
|
||||
|
||||
|
||||
var image: UIImage {
|
||||
return UIColor.image(color: self)
|
||||
}
|
||||
|
||||
@ -16,4 +16,4 @@ protocol ViewModelType {
|
||||
func transform(input: Input) -> Output
|
||||
}
|
||||
|
||||
class ViewModel:NSObject{ }
|
||||
class ViewModel: NSObject {}
|
||||
|
||||
@ -6,11 +6,11 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
class BarkNavigationController: UINavigationController{
|
||||
class BarkNavigationController: UINavigationController {
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.navigationBar.prefersLargeTitles = true
|
||||
@ -18,7 +18,7 @@ class BarkNavigationController: UINavigationController{
|
||||
}
|
||||
|
||||
class BarkSnackbarController: SnackbarController {
|
||||
override var childForStatusBarStyle: UIViewController?{
|
||||
override var childForStatusBarStyle: UIViewController? {
|
||||
return self.rootViewController
|
||||
}
|
||||
}
|
||||
@ -31,13 +31,12 @@ enum TabPage: Int {
|
||||
}
|
||||
|
||||
class StateStorageTabBarController: UITabBarController, UITabBarControllerDelegate {
|
||||
|
||||
// 标记当前显示的页面,再次点击相同的页面时当做页面点击事件。
|
||||
var currentSelectedIndex: Int = 0
|
||||
|
||||
|
||||
// 点击当前页面的 tabBarItem , 可以用以点击刷新当前页面等操作
|
||||
lazy var tabBarItemDidClick: Observable<TabPage> = {
|
||||
return self.rx.didSelect
|
||||
self.rx.didSelect
|
||||
.flatMapLatest { _ -> Single<TabPage> in
|
||||
let single = Single<TabPage>.create { single in
|
||||
if self.currentSelectedIndex == self.selectedIndex {
|
||||
@ -55,17 +54,16 @@ class StateStorageTabBarController: UITabBarController, UITabBarControllerDelega
|
||||
super.viewWillAppear(animated)
|
||||
if isFirstAppear {
|
||||
isFirstAppear = false
|
||||
|
||||
//开启APP时,默认选择上次打开的页面
|
||||
if let index:Int = Settings[.selectedViewControllerIndex] {
|
||||
|
||||
// 开启APP时,默认选择上次打开的页面
|
||||
if let index: Int = Settings[.selectedViewControllerIndex] {
|
||||
self.selectedIndex = index
|
||||
self.currentSelectedIndex = index
|
||||
}
|
||||
//保存打开的页面Index
|
||||
self.rx.didSelect.subscribe(onNext: {_ in
|
||||
// 保存打开的页面Index
|
||||
self.rx.didSelect.subscribe(onNext: { _ in
|
||||
Settings[.selectedViewControllerIndex] = self.selectedIndex
|
||||
}).disposed(by: rx.disposeBag)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,10 +6,9 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import SafariServices
|
||||
import UIKit
|
||||
class BarkSFSafariViewController: SFSafariViewController {
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
|
||||
@ -20,11 +19,8 @@ class BarkSFSafariViewController: SFSafariViewController {
|
||||
super.didReceiveMemoryWarning()
|
||||
// Dispose of any resources that can be recreated.
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle{
|
||||
get {
|
||||
return .default
|
||||
}
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .default
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,26 +6,24 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import UIKit
|
||||
class BaseViewController: UIViewController {
|
||||
|
||||
let viewModel:ViewModel
|
||||
init(viewModel:ViewModel) {
|
||||
let viewModel: ViewModel
|
||||
init(viewModel: ViewModel) {
|
||||
self.viewModel = viewModel
|
||||
super.init(nibName: nil, bundle: nil)
|
||||
|
||||
self.view.backgroundColor = Color.grey.lighten5
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle{
|
||||
get {
|
||||
return .lightContent
|
||||
}
|
||||
override var preferredStatusBarStyle: UIStatusBarStyle {
|
||||
return .lightContent
|
||||
}
|
||||
|
||||
override func viewDidLoad() {
|
||||
@ -33,6 +31,7 @@ class BaseViewController: UIViewController {
|
||||
self.navigationItem.largeTitleDisplayMode = .automatic
|
||||
makeUI()
|
||||
}
|
||||
|
||||
var isViewModelBinded = false
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
@ -42,10 +41,7 @@ class BaseViewController: UIViewController {
|
||||
}
|
||||
}
|
||||
|
||||
func makeUI() {
|
||||
|
||||
}
|
||||
func bindViewModel(){
|
||||
|
||||
}
|
||||
func makeUI() {}
|
||||
|
||||
func bindViewModel() {}
|
||||
}
|
||||
|
||||
@ -6,16 +6,15 @@
|
||||
// Copyright © 2021 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import MJRefresh
|
||||
import RealmSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import MJRefresh
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
class GroupFilterViewController: BaseViewController {
|
||||
|
||||
let doneButton: BKButton = {
|
||||
let btn = BKButton()
|
||||
btn.setTitle(NSLocalizedString("done"), for: .normal)
|
||||
@ -35,7 +34,7 @@ class GroupFilterViewController: BaseViewController {
|
||||
}()
|
||||
|
||||
let tableView: UITableView = {
|
||||
let tableView:UITableView = UITableView(frame: CGRect.zero, style: .insetGrouped)
|
||||
let tableView = UITableView(frame: CGRect.zero, style: .insetGrouped)
|
||||
tableView.separatorStyle = .singleLine
|
||||
tableView.separatorColor = Color.grey.lighten3
|
||||
tableView.backgroundColor = Color.grey.lighten5
|
||||
@ -49,9 +48,9 @@ class GroupFilterViewController: BaseViewController {
|
||||
|
||||
self.view.addSubview(tableView)
|
||||
self.view.addSubview(showAllGroupsButton)
|
||||
tableView.snp.makeConstraints { (make) in
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.top.equalToSuperview()
|
||||
make.bottom.equalToSuperview().offset( (kSafeAreaInsets.bottom + 40) * -1)
|
||||
make.bottom.equalToSuperview().offset((kSafeAreaInsets.bottom + 40) * -1)
|
||||
make.left.right.equalToSuperview()
|
||||
}
|
||||
showAllGroupsButton.snp.makeConstraints { make in
|
||||
@ -62,6 +61,7 @@ class GroupFilterViewController: BaseViewController {
|
||||
|
||||
self.tableView.tableHeaderView = UIView(frame: CGRect(x: 0, y: 0, width: self.view.bounds.size.width, height: 20))
|
||||
}
|
||||
|
||||
override func bindViewModel() {
|
||||
guard let viewModel = self.viewModel as? GroupFilterViewModel else {
|
||||
return
|
||||
@ -71,15 +71,15 @@ class GroupFilterViewController: BaseViewController {
|
||||
input: GroupFilterViewModel.Input(
|
||||
showAllGroups: self.showAllGroupsButton.rx
|
||||
.tap
|
||||
.compactMap({[weak self] in
|
||||
guard let strongSelf = self else {return nil}
|
||||
.compactMap { [weak self] in
|
||||
guard let strongSelf = self else { return nil }
|
||||
return !strongSelf.showAllGroupsButton.isSelected
|
||||
})
|
||||
}
|
||||
.asDriver(onErrorDriveWith: .empty()),
|
||||
doneTap: self.doneButton.rx.tap.asDriver()
|
||||
))
|
||||
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, GroupCellViewModel>> { (source, tableView, indexPath, item) -> UITableViewCell in
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, GroupCellViewModel>> { _, tableView, _, item -> UITableViewCell in
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(GroupTableViewCell.self)") as? GroupTableViewCell else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
@ -95,13 +95,11 @@ class GroupFilterViewController: BaseViewController {
|
||||
.drive(self.showAllGroupsButton.rx.isSelected)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
output.dismiss.drive(onNext: {[weak self] in
|
||||
output.dismiss.drive(onNext: { [weak self] in
|
||||
self?.dismiss(animated: true, completion: nil)
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
}
|
||||
}
|
||||
|
||||
extension GroupFilterViewController: UITableViewDelegate {
|
||||
|
||||
}
|
||||
extension GroupFilterViewController: UITableViewDelegate {}
|
||||
|
||||
@ -6,89 +6,86 @@
|
||||
// Copyright © 2021 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxDataSources
|
||||
import RxCocoa
|
||||
import RealmSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import RxSwift
|
||||
import UIKit
|
||||
struct GroupFilterModel {
|
||||
var name:String?
|
||||
var checked:Bool
|
||||
var name: String?
|
||||
var checked: Bool
|
||||
}
|
||||
class GroupFilterViewModel: ViewModel,ViewModelType {
|
||||
let groups:[GroupFilterModel]
|
||||
init(groups:[GroupFilterModel]) {
|
||||
|
||||
class GroupFilterViewModel: ViewModel, ViewModelType {
|
||||
let groups: [GroupFilterModel]
|
||||
init(groups: [GroupFilterModel]) {
|
||||
self.groups = groups
|
||||
}
|
||||
|
||||
struct Input {
|
||||
var showAllGroups:Driver<Bool>
|
||||
var doneTap:Driver<Void>
|
||||
var showAllGroups: Driver<Bool>
|
||||
var doneTap: Driver<Void>
|
||||
}
|
||||
|
||||
struct Output {
|
||||
var groups:Driver<[ SectionModel<String, GroupCellViewModel>]>
|
||||
var isShowAllGroups:Driver<Bool>
|
||||
var dismiss:Driver<Void>
|
||||
var groups: Driver<[SectionModel<String, GroupCellViewModel>]>
|
||||
var isShowAllGroups: Driver<Bool>
|
||||
var dismiss: Driver<Void>
|
||||
}
|
||||
|
||||
var done = PublishRelay<[String?]>()
|
||||
|
||||
func transform(input: Input) -> Output {
|
||||
|
||||
// 页面中的群组cellModel
|
||||
let groupCellModels = self.groups.map({ filterModel in
|
||||
return GroupCellViewModel(groupFilterModel: filterModel)
|
||||
})
|
||||
let groupCellModels = self.groups.map { filterModel in
|
||||
GroupCellViewModel(groupFilterModel: filterModel)
|
||||
}
|
||||
|
||||
//点击显示所有群组或隐藏所有群组时,设置cell checked 勾选状态
|
||||
// 点击显示所有群组或隐藏所有群组时,设置cell checked 勾选状态
|
||||
input.showAllGroups.drive(onNext: { isShowAllGroups in
|
||||
groupCellModels.forEach { model in
|
||||
groupCellModels.forEach { model in
|
||||
model.checked.accept(isShowAllGroups)
|
||||
}
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//cell checked 状态改变
|
||||
// cell checked 状态改变
|
||||
let checkChanged = Observable.merge(groupCellModels.map { model in
|
||||
return model.checked.asObservable()
|
||||
model.checked.asObservable()
|
||||
})
|
||||
|
||||
// 是否勾选了所有群组
|
||||
let isShowAllGroups =
|
||||
checkChanged
|
||||
.map{_ in
|
||||
return groupCellModels.filter { viewModel in
|
||||
return viewModel.checked.value
|
||||
}.count >= groupCellModels.count
|
||||
}
|
||||
.map { _ in
|
||||
groupCellModels.filter { viewModel in
|
||||
viewModel.checked.value
|
||||
}.count >= groupCellModels.count
|
||||
}
|
||||
input.doneTap.map { () -> [String?] in
|
||||
let isShowAllGroups = groupCellModels.filter { viewModel in
|
||||
return viewModel.checked.value
|
||||
viewModel.checked.value
|
||||
}.count >= groupCellModels.count
|
||||
if isShowAllGroups {
|
||||
return []
|
||||
}
|
||||
return groupCellModels
|
||||
.filter { $0.checked.value}
|
||||
.filter { $0.checked.value }
|
||||
.map { $0.name.value }
|
||||
}
|
||||
.asObservable()
|
||||
.bind(to: self.done)
|
||||
.disposed(by: rx.disposeBag);
|
||||
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
let dismiss = PublishRelay<Void>()
|
||||
input.doneTap.map{ _ in () }
|
||||
input.doneTap.map { _ in () }
|
||||
.asObservable()
|
||||
.bind(to: dismiss)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
return Output(
|
||||
groups: Driver.just([ SectionModel(model: "header", items: groupCellModels) ]),
|
||||
groups: Driver.just([SectionModel(model: "header", items: groupCellModels)]),
|
||||
isShowAllGroups: isShowAllGroups.asDriver(onErrorDriveWith: .empty()),
|
||||
dismiss: dismiss.asDriver(onErrorDriveWith: .empty())
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
@ -6,14 +6,13 @@
|
||||
// Copyright © 2018年 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import Material
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
|
||||
class HomeViewController: BaseViewController {
|
||||
|
||||
let newButton: BKButton = {
|
||||
let btn = BKButton()
|
||||
btn.setImage(Icon.add, for: .normal)
|
||||
@ -24,11 +23,11 @@ class HomeViewController: BaseViewController {
|
||||
let startButton: FABButton = {
|
||||
let button = FABButton(title: NSLocalizedString("RegisterDevice"))
|
||||
button.backgroundColor = Color.white
|
||||
button.transition([ .scale(0.75) , .opacity(0)] )
|
||||
button.transition([.scale(0.75), .opacity(0)])
|
||||
return button
|
||||
}()
|
||||
|
||||
let tableView :UITableView = {
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
tableView.separatorStyle = .none
|
||||
tableView.backgroundColor = Color.grey.lighten4
|
||||
@ -44,12 +43,12 @@ class HomeViewController: BaseViewController {
|
||||
item: UIBarButtonItem(customView: newButton))
|
||||
|
||||
self.view.addSubview(self.tableView)
|
||||
self.tableView.snp.makeConstraints { (make ) in
|
||||
self.tableView.snp.makeConstraints { make in
|
||||
make.top.right.bottom.left.equalToSuperview()
|
||||
}
|
||||
|
||||
self.view.addSubview(self.startButton)
|
||||
self.startButton.snp.makeConstraints { (make) in
|
||||
self.startButton.snp.makeConstraints { make in
|
||||
make.width.height.equalTo(150)
|
||||
make.centerX.equalToSuperview()
|
||||
make.centerY.equalToSuperview().offset(-50)
|
||||
@ -57,12 +56,12 @@ class HomeViewController: BaseViewController {
|
||||
|
||||
Client.shared.currentTabBarController?
|
||||
.tabBarItemDidClick
|
||||
.filter{ $0 == .service }
|
||||
.subscribe(onNext: {[weak self] index in
|
||||
.filter { $0 == .service }
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
||||
}).disposed(by: self.rx.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
override func bindViewModel() {
|
||||
guard let viewModel = self.viewModel as? HomeViewModel else {
|
||||
return
|
||||
@ -72,96 +71,95 @@ class HomeViewController: BaseViewController {
|
||||
input: HomeViewModel.Input(
|
||||
addCustomServerTap: newButton.rx.tap.asDriver(),
|
||||
viewDidAppear: self.rx.methodInvoked(#selector(viewDidAppear(_:)))
|
||||
.map{ _ in () }
|
||||
.map { _ in () }
|
||||
.asDriver(onErrorDriveWith: .empty()),
|
||||
start: self.startButton.rx.tap.asDriver(),
|
||||
clientState: Client.shared.state.asDriver()
|
||||
)
|
||||
)
|
||||
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String,PreviewCardCellViewModel>> { (source, tableView, indexPath, item) -> UITableViewCell in
|
||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "\(PreviewCardCell.self)") as? PreviewCardCell{
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, PreviewCardCellViewModel>> { _, tableView, _, item -> UITableViewCell in
|
||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "\(PreviewCardCell.self)") as? PreviewCardCell {
|
||||
cell.bindViewModel(model: item)
|
||||
return cell
|
||||
}
|
||||
return UITableViewCell()
|
||||
}
|
||||
|
||||
//标题
|
||||
// 标题
|
||||
output.title
|
||||
.drive(self.navigationItem.rx.title)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//TableView数据源
|
||||
// TableView数据源
|
||||
output.previews
|
||||
.drive(self.tableView.rx.items(dataSource: dataSource))
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//跳转到对应页面
|
||||
// 跳转到对应页面
|
||||
output.push
|
||||
.drive(onNext: {[weak self] viewModel in
|
||||
.drive(onNext: { [weak self] viewModel in
|
||||
self?.pushViewModel(viewModel: viewModel)
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//通过ping服务器,判断 clienState
|
||||
// 通过ping服务器,判断 clienState
|
||||
output.clienStateChanged
|
||||
.drive(Client.shared.state)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//根据通知权限,设置是否隐藏注册按钮、显示示例预览列表
|
||||
// 根据通知权限,设置是否隐藏注册按钮、显示示例预览列表
|
||||
output.tableViewHidden
|
||||
.map{ !$0 }
|
||||
.map { !$0 }
|
||||
.drive(self.tableView.rx.isHidden)
|
||||
.disposed(by: rx.disposeBag)
|
||||
output.tableViewHidden
|
||||
.drive(self.startButton.rx.isHidden)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//注册推送
|
||||
// 注册推送
|
||||
output.registerForRemoteNotifications.drive(onNext: {
|
||||
UIApplication.shared.registerForRemoteNotifications()
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//弹出提示
|
||||
// 弹出提示
|
||||
output.showSnackbar
|
||||
.drive(onNext: {[weak self] text in
|
||||
.drive(onNext: { [weak self] text in
|
||||
self?.showSnackbar(text: text)
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//startButton是否可点击
|
||||
// startButton是否可点击
|
||||
output.startButtonEnable
|
||||
.drive(self.startButton.rx.isEnabled)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//复制文本
|
||||
// 复制文本
|
||||
output.copy
|
||||
.drive(onNext: {[weak self] text in
|
||||
.drive(onNext: { [weak self] text in
|
||||
UIPasteboard.general.string = text
|
||||
self?.showSnackbar(text: NSLocalizedString("Copy"))
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//预览
|
||||
// 预览
|
||||
output.preview
|
||||
.drive(onNext: { url in
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//原样刷新 TableView
|
||||
// 原样刷新 TableView
|
||||
output.reloadData
|
||||
.drive(onNext: {[weak self] in
|
||||
.drive(onNext: { [weak self] in
|
||||
self?.tableView.reloadData()
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
func pushViewModel(viewModel:ViewModel) {
|
||||
var viewController:UIViewController?
|
||||
func pushViewModel(viewModel: ViewModel) {
|
||||
var viewController: UIViewController?
|
||||
if let viewModel = viewModel as? NewServerViewModel {
|
||||
viewController = NewServerViewController(viewModel: viewModel)
|
||||
}
|
||||
@ -173,5 +171,4 @@ class HomeViewController: BaseViewController {
|
||||
self.navigationController?.pushViewController(viewController, animated: true)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import RxSwift
|
||||
import SwiftyJSON
|
||||
import UserNotifications
|
||||
|
||||
@ -20,8 +20,9 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
let start: Driver<Void>
|
||||
let clientState: Driver<Client.ClienState>
|
||||
}
|
||||
|
||||
struct Output {
|
||||
let previews: Driver<[SectionModel<String,PreviewCardCellViewModel>]>
|
||||
let previews: Driver<[SectionModel<String, PreviewCardCellViewModel>]>
|
||||
let push: Driver<ViewModel>
|
||||
let title: Driver<String>
|
||||
let clienStateChanged: Driver<Client.ClienState>
|
||||
@ -34,44 +35,46 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
let registerForRemoteNotifications: Driver<Void>
|
||||
}
|
||||
|
||||
let previews:[PreviewModel] = {
|
||||
return [
|
||||
let previews: [PreviewModel] = {
|
||||
[
|
||||
PreviewModel(
|
||||
body: NSLocalizedString("CustomedNotificationContent"),
|
||||
notice: NSLocalizedString("Notice1")),
|
||||
notice: NSLocalizedString("Notice1")
|
||||
),
|
||||
PreviewModel(
|
||||
title: NSLocalizedString("CustomedNotificationTitle"),
|
||||
body: NSLocalizedString("CustomedNotificationContent"),
|
||||
notice: NSLocalizedString("Notice2")),
|
||||
notice: NSLocalizedString("Notice2")
|
||||
),
|
||||
PreviewModel(
|
||||
body: NSLocalizedString("notificationSound"),
|
||||
notice: NSLocalizedString("setSounds"),
|
||||
queryParameter: "sound=minuet",
|
||||
moreInfo:NSLocalizedString("viewAllSounds"),
|
||||
moreInfo: NSLocalizedString("viewAllSounds"),
|
||||
moreViewModel: SoundsViewModel()
|
||||
),
|
||||
PreviewModel(
|
||||
body: NSLocalizedString("archiveNotificationMessageTitle"),
|
||||
notice: NSLocalizedString("archiveNotificationMessage"),
|
||||
queryParameter: "isArchive=1"
|
||||
),
|
||||
),
|
||||
PreviewModel(
|
||||
body: NSLocalizedString("notificationIcon"),
|
||||
notice: NSLocalizedString("notificationIconNotice"),
|
||||
queryParameter: "icon=https://day.app/assets/images/avatar.jpg",
|
||||
image: UIImage(named: "icon")
|
||||
),
|
||||
),
|
||||
PreviewModel(
|
||||
body: NSLocalizedString("messageGroup"),
|
||||
notice: NSLocalizedString("groupMessagesNotice"),
|
||||
queryParameter: "group=groupName",
|
||||
image: UIImage(named: "group")
|
||||
),
|
||||
),
|
||||
PreviewModel(
|
||||
body: "URL Test",
|
||||
notice: NSLocalizedString("urlParameter"),
|
||||
queryParameter: "url=https://www.baidu.com"
|
||||
),
|
||||
),
|
||||
PreviewModel(
|
||||
body: "Copy Test",
|
||||
notice: NSLocalizedString("copyParameter"),
|
||||
@ -87,36 +90,35 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
}()
|
||||
|
||||
func transform(input: Input) -> Output {
|
||||
|
||||
let title = BehaviorRelay(value: URL(string: ServerManager.shared.currentAddress)?.host ?? "")
|
||||
|
||||
let sectionModel = SectionModel(
|
||||
model: "previews",
|
||||
items: previews.map { PreviewCardCellViewModel(previewModel: $0, clientState: input.clientState) })
|
||||
items: previews.map { PreviewCardCellViewModel(previewModel: $0, clientState: input.clientState) }
|
||||
)
|
||||
|
||||
// 点击跳转到添加自定义服务器
|
||||
let customServer = input.addCustomServerTap.map { NewServerViewModel() as ViewModel }
|
||||
|
||||
//点击跳转到添加自定义服务器
|
||||
let customServer = input.addCustomServerTap.map{ NewServerViewModel() as ViewModel }
|
||||
|
||||
//如果更改了服务器地址,返回时也需更改 title
|
||||
// 如果更改了服务器地址,返回时也需更改 title
|
||||
customServer
|
||||
.flatMapLatest({ (model) -> Driver<String> in
|
||||
return (model as! NewServerViewModel).pop.asDriver(onErrorJustReturn: "")
|
||||
})
|
||||
.flatMapLatest { model -> Driver<String> in
|
||||
(model as! NewServerViewModel).pop.asDriver(onErrorJustReturn: "")
|
||||
}
|
||||
.drive(title)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//点击preview中的notice ,跳转到对应的页面
|
||||
let noticeTap = Driver.merge(sectionModel.items.map{ $0.noticeTap.asDriver(onErrorDriveWith: .empty()) })
|
||||
// 点击preview中的notice ,跳转到对应的页面
|
||||
let noticeTap = Driver.merge(sectionModel.items.map { $0.noticeTap.asDriver(onErrorDriveWith: .empty()) })
|
||||
|
||||
// 判断服务器状态
|
||||
let clienState = input.viewDidAppear
|
||||
.asObservable().flatMapLatest { _ -> Observable<Result<JSON,ApiError>> in
|
||||
.asObservable().flatMapLatest { _ -> Observable<Result<JSON, ApiError>> in
|
||||
BarkApi.provider
|
||||
.request(.ping(baseURL: ServerManager.shared.currentAddress))
|
||||
.filterResponseError()
|
||||
}
|
||||
.map { (response) -> Client.ClienState in
|
||||
.map { response -> Client.ClienState in
|
||||
switch response {
|
||||
case .failure:
|
||||
return .serverError
|
||||
@ -125,40 +127,39 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
}
|
||||
}
|
||||
|
||||
//第一次进入APP 查看通知权限设置
|
||||
let authorizationStatus = Single<UNAuthorizationStatus>.create { (single) -> Disposable in
|
||||
UNUserNotificationCenter.current().getNotificationSettings { (settings) in
|
||||
// 第一次进入APP 查看通知权限设置
|
||||
let authorizationStatus = Single<UNAuthorizationStatus>.create { single -> Disposable in
|
||||
UNUserNotificationCenter.current().getNotificationSettings { settings in
|
||||
single(.success(settings.authorizationStatus))
|
||||
}
|
||||
return Disposables.create()
|
||||
}
|
||||
.map { $0 == .authorized}
|
||||
.map { $0 == .authorized }
|
||||
|
||||
//点击注册按钮,请求通知权限
|
||||
let startRequestAuthorization = Single<Bool>.create { (single) -> Disposable in
|
||||
// 点击注册按钮,请求通知权限
|
||||
let startRequestAuthorization = Single<Bool>.create { single -> Disposable in
|
||||
let center = UNUserNotificationCenter.current()
|
||||
center.requestAuthorization(options: [.alert , .sound , .badge], completionHandler: {(_ granted: Bool, _ error: Error?) -> Void in
|
||||
center.requestAuthorization(options: [.alert, .sound, .badge], completionHandler: { (_ granted: Bool, _: Error?) -> Void in
|
||||
single(.success(granted))
|
||||
})
|
||||
return Disposables.create()
|
||||
}
|
||||
.asObservable()
|
||||
|
||||
//根据通知权限,设置是否隐藏注册按钮、显示示例预览列表
|
||||
// 根据通知权限,设置是否隐藏注册按钮、显示示例预览列表
|
||||
let tableViewHidden = authorizationStatus
|
||||
.asObservable()
|
||||
.concat(input.start
|
||||
.asObservable()
|
||||
.flatMapLatest{ startRequestAuthorization })
|
||||
.asObservable()
|
||||
.flatMapLatest { startRequestAuthorization })
|
||||
.asDriver(onErrorJustReturn: false)
|
||||
|
||||
|
||||
let showSnackbar = PublishRelay<String>()
|
||||
|
||||
//点击注册按钮后,如果不允许推送,弹出提示
|
||||
// 点击注册按钮后,如果不允许推送,弹出提示
|
||||
tableViewHidden
|
||||
.skip(1)
|
||||
.compactMap { (granted) -> String? in
|
||||
.compactMap { granted -> String? in
|
||||
if !granted {
|
||||
return NSLocalizedString("AllowNotifications")
|
||||
}
|
||||
@ -168,36 +169,35 @@ class HomeViewModel: ViewModel, ViewModelType {
|
||||
.bind(to: showSnackbar)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//点击注册按钮,如果用户允许推送,则通知 viewController 注册推送
|
||||
// 点击注册按钮,如果用户允许推送,则通知 viewController 注册推送
|
||||
let registerForRemoteNotifications = tableViewHidden
|
||||
.skip(1)
|
||||
.filter{ $0 }
|
||||
.map{ _ in () }
|
||||
.filter { $0 }
|
||||
.map { _ in () }
|
||||
|
||||
//client state 变化时,发出相应错误提醒
|
||||
// client state 变化时,发出相应错误提醒
|
||||
input.clientState.drive(onNext: { state in
|
||||
switch state {
|
||||
case .ok: break;
|
||||
case .ok: break
|
||||
case .serverError:
|
||||
showSnackbar.accept(NSLocalizedString("ServerError"))
|
||||
default: break;
|
||||
default: break
|
||||
}
|
||||
})
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
return Output(
|
||||
previews:Driver.just([sectionModel]),
|
||||
push: Driver<ViewModel>.merge(customServer,noticeTap),
|
||||
previews: Driver.just([sectionModel]),
|
||||
push: Driver<ViewModel>.merge(customServer, noticeTap),
|
||||
title: title.asDriver(),
|
||||
clienStateChanged: clienState.asDriver(onErrorDriveWith: .empty()),
|
||||
tableViewHidden: tableViewHidden,
|
||||
showSnackbar: showSnackbar.asDriver(onErrorDriveWith: .empty()),
|
||||
startButtonEnable: Driver.just(true),
|
||||
copy: Driver.merge(sectionModel.items.map{ $0.copy.asDriver(onErrorDriveWith: .empty()) }),
|
||||
preview: Driver.merge(sectionModel.items.map{ $0.preview.asDriver(onErrorDriveWith: .empty()) }),
|
||||
reloadData: input.clientState.map{ _ in ()},
|
||||
copy: Driver.merge(sectionModel.items.map { $0.copy.asDriver(onErrorDriveWith: .empty()) }),
|
||||
preview: Driver.merge(sectionModel.items.map { $0.preview.asDriver(onErrorDriveWith: .empty()) }),
|
||||
reloadData: input.clientState.map { _ in () },
|
||||
registerForRemoteNotifications: registerForRemoteNotifications
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -6,29 +6,27 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import MJRefresh
|
||||
import RealmSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import MJRefresh
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
enum MessageDeleteType: Int{
|
||||
enum MessageDeleteType: Int {
|
||||
case lastHour = 0
|
||||
case today
|
||||
case todayAndYesterday
|
||||
case allTime
|
||||
|
||||
var string: String{
|
||||
get {
|
||||
return [
|
||||
NSLocalizedString("lastHour"),
|
||||
NSLocalizedString("today"),
|
||||
NSLocalizedString("todayAndYesterday"),
|
||||
NSLocalizedString("allTime"),
|
||||
][self.rawValue]
|
||||
}
|
||||
var string: String {
|
||||
return [
|
||||
NSLocalizedString("lastHour"),
|
||||
NSLocalizedString("today"),
|
||||
NSLocalizedString("todayAndYesterday"),
|
||||
NSLocalizedString("allTime"),
|
||||
][self.rawValue]
|
||||
}
|
||||
}
|
||||
|
||||
@ -64,7 +62,7 @@ class MessageListViewController: BaseViewController {
|
||||
navigationItem.setBarButtonItems(items: [UIBarButtonItem(customView: deleteButton), UIBarButtonItem(customView: groupButton)], left: false)
|
||||
|
||||
self.view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { (make) in
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
tableView.rx.setDelegate(self).disposed(by: rx.disposeBag)
|
||||
@ -74,13 +72,12 @@ class MessageListViewController: BaseViewController {
|
||||
// 点击tab按钮,回到顶部
|
||||
Client.shared.currentTabBarController?
|
||||
.tabBarItemDidClick
|
||||
.filter{ $0 == .messageHistory }
|
||||
.subscribe(onNext: {[weak self] index in
|
||||
.filter { $0 == .messageHistory }
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.scrollToTop()
|
||||
}).disposed(by: self.rx.disposeBag)
|
||||
|
||||
|
||||
//打开APP时,历史消息列表距离上次刷新超过1小时,则自动刷新一下
|
||||
// 打开APP时,历史消息列表距离上次刷新超过1小时,则自动刷新一下
|
||||
var lastAutoRefreshdate = Date()
|
||||
NotificationCenter.default.rx
|
||||
.notification(UIApplication.willEnterForegroundNotification)
|
||||
@ -92,11 +89,10 @@ class MessageListViewController: BaseViewController {
|
||||
}
|
||||
return false
|
||||
}
|
||||
.subscribe(onNext: {[weak self] _ in
|
||||
.subscribe(onNext: { [weak self] _ in
|
||||
self?.tableView.refreshControl?.sendActions(for: .valueChanged)
|
||||
self?.scrollToTop()
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
override func bindViewModel() {
|
||||
@ -106,10 +102,10 @@ class MessageListViewController: BaseViewController {
|
||||
|
||||
let batchDelete = deleteButton.rx
|
||||
.tap
|
||||
.flatMapLatest { Void -> PublishRelay<MessageDeleteType> in
|
||||
.flatMapLatest { _ -> PublishRelay<MessageDeleteType> in
|
||||
let relay = PublishRelay<MessageDeleteType>()
|
||||
|
||||
func alert(_ type:MessageDeleteType){
|
||||
func alert(_ type: MessageDeleteType) {
|
||||
let alertController = UIAlertController(title: nil, message: "\(NSLocalizedString("clearFrom"))\n\(type.string)", preferredStyle: .alert)
|
||||
alertController.addAction(UIAlertAction(title: NSLocalizedString("clear"), style: .destructive, handler: { _ in
|
||||
relay.accept(type)
|
||||
@ -143,67 +139,65 @@ class MessageListViewController: BaseViewController {
|
||||
loadMore: tableView.mj_footer!.rx.refresh.asDriver(),
|
||||
itemDelete: tableView.rx.itemDeleted.asDriver(),
|
||||
itemSelected: tableView.rx.modelSelected(MessageTableViewCellViewModel.self).asDriver(),
|
||||
delete:batchDelete.asDriver(onErrorDriveWith: .empty()),
|
||||
delete: batchDelete.asDriver(onErrorDriveWith: .empty()),
|
||||
groupTap: groupButton.rx.tap.asDriver(),
|
||||
searchText: navigationItem.searchController!.searchBar.rx.text.asObservable()
|
||||
))
|
||||
searchText: navigationItem.searchController!.searchBar.rx.text.asObservable()))
|
||||
|
||||
//tableView 刷新状态
|
||||
// tableView 刷新状态
|
||||
output.refreshAction
|
||||
.drive(tableView.rx.refreshAction)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//tableView 数据源
|
||||
// tableView 数据源
|
||||
let dataSource = RxTableViewSectionedAnimatedDataSource<MessageSection>(
|
||||
animationConfiguration: AnimationConfiguration(
|
||||
insertAnimation: .none,
|
||||
reloadAnimation: .none,
|
||||
deleteAnimation: .left),
|
||||
configureCell:{ (source, tableView, indexPath, item) -> UITableViewCell in
|
||||
configureCell: { _, tableView, _, item -> UITableViewCell in
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(MessageTableViewCell.self)") as? MessageTableViewCell else {
|
||||
return UITableViewCell ()
|
||||
return UITableViewCell()
|
||||
}
|
||||
cell.bindViewModel(model: item)
|
||||
return cell
|
||||
}, canEditRowAtIndexPath: { _, _ in
|
||||
return true
|
||||
true
|
||||
})
|
||||
|
||||
output.messages
|
||||
.drive(tableView.rx.items(dataSource: dataSource))
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//message操作alert
|
||||
output.alertMessage.drive(onNext: {[weak self] message in
|
||||
// message操作alert
|
||||
output.alertMessage.drive(onNext: { [weak self] message in
|
||||
self?.alertMessage(message: message)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//点击message中的URL
|
||||
// 点击message中的URL
|
||||
output.urlTap.drive(onNext: { url in
|
||||
if ["http","https"].contains(url.scheme?.lowercased() ?? ""){
|
||||
if ["http", "https"].contains(url.scheme?.lowercased() ?? "") {
|
||||
self.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)
|
||||
}
|
||||
else{
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}
|
||||
else {
|
||||
UIApplication.shared.open(url, options: [:], completionHandler: nil)
|
||||
}
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//选择群组
|
||||
// 选择群组
|
||||
output.groupFilter
|
||||
.drive(onNext: {[weak self] groupModel in
|
||||
.drive(onNext: { [weak self] groupModel in
|
||||
self?.navigationController?.present(BarkNavigationController(rootViewController: GroupFilterViewController(viewModel: groupModel)), animated: true, completion: nil)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//标题
|
||||
// 标题
|
||||
output.title
|
||||
.drive(self.navigationItem.rx.title).disposed(by: rx.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
func alertMessage(message:String) {
|
||||
func alertMessage(message: String) {
|
||||
let alertController = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet)
|
||||
let copyAction = UIAlertAction(title: NSLocalizedString("Copy2"), style: .default, handler: {[weak self]
|
||||
(alert: UIAlertAction) -> Void in
|
||||
let copyAction = UIAlertAction(title: NSLocalizedString("Copy2"), style: .default, handler: { [weak self]
|
||||
(_: UIAlertAction) -> Void in
|
||||
UIPasteboard.general.string = message
|
||||
self?.showSnackbar(text: NSLocalizedString("Copy"))
|
||||
})
|
||||
@ -216,7 +210,7 @@ class MessageListViewController: BaseViewController {
|
||||
self.navigationController?.present(alertController, animated: true, completion: nil)
|
||||
}
|
||||
|
||||
private func scrollToTop(){
|
||||
private func scrollToTop() {
|
||||
if self.tableView.visibleCells.count > 0 {
|
||||
self.tableView.scrollToRow(at: IndexPath(row: 0, section: 0), at: .top, animated: true)
|
||||
}
|
||||
@ -225,7 +219,7 @@ class MessageListViewController: BaseViewController {
|
||||
|
||||
extension MessageListViewController: UITableViewDelegate {
|
||||
func tableView(_ tableView: UITableView, trailingSwipeActionsConfigurationForRowAt indexPath: IndexPath) -> UISwipeActionsConfiguration? {
|
||||
let action = UIContextualAction(style: .destructive, title: "删除") {[weak self] (action, sourceView, actionPerformed) in
|
||||
let action = UIContextualAction(style: .destructive, title: "删除") { [weak self] _, _, actionPerformed in
|
||||
self?.tableView.dataSource?.tableView?(self!.tableView, commit: .delete, forRowAt: indexPath)
|
||||
actionPerformed(true)
|
||||
}
|
||||
@ -235,14 +229,15 @@ extension MessageListViewController: UITableViewDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
extension MessageListViewController: UISearchControllerDelegate{
|
||||
extension MessageListViewController: UISearchControllerDelegate {
|
||||
func scrollViewWillBeginDragging(_ scrollView: UIScrollView) {
|
||||
if self.navigationItem.searchController?.searchBar.isFirstResponder == true{
|
||||
if self.navigationItem.searchController?.searchBar.isFirstResponder == true {
|
||||
self.navigationItem.searchController?.searchBar.resignFirstResponder()
|
||||
}
|
||||
}
|
||||
|
||||
func willDismissSearchController(_ searchController: UISearchController) {
|
||||
if !searchController.searchBar.isFirstResponder{
|
||||
if !searchController.searchBar.isFirstResponder {
|
||||
/*
|
||||
searchBar 不在焦点时,点击搜索框右边的取消按钮时,不会触发 searchBar.rx.text 更改事件
|
||||
searchBar.rx.text 将一直保留为最后的文本
|
||||
|
||||
@ -7,12 +7,12 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import RxDataSources
|
||||
import RxCocoa
|
||||
import RealmSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import RxSwift
|
||||
|
||||
class MessageListViewModel: ViewModel,ViewModelType {
|
||||
class MessageListViewModel: ViewModel, ViewModelType {
|
||||
struct Input {
|
||||
var refresh: Driver<Void>
|
||||
var loadMore: Driver<Void>
|
||||
@ -32,10 +32,10 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
var title: Driver<String>
|
||||
}
|
||||
|
||||
private var results:Results<Message>?
|
||||
private var results: Results<Message>?
|
||||
|
||||
//根据群组获取消息
|
||||
private func getResults(filterGroups:[String?], searchText:String?) -> Results<Message>? {
|
||||
// 根据群组获取消息
|
||||
private func getResults(filterGroups: [String?], searchText: String?) -> Results<Message>? {
|
||||
if let realm = try? Realm() {
|
||||
var results = realm.objects(Message.self)
|
||||
.filter("isDeleted != true")
|
||||
@ -60,7 +60,7 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
guard endIndex > startIndex else {
|
||||
return []
|
||||
}
|
||||
var messages:[Message] = []
|
||||
var messages: [Message] = []
|
||||
for i in startIndex ..< endIndex {
|
||||
messages.append(result[i])
|
||||
}
|
||||
@ -70,12 +70,11 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
func transform(input: Input) -> Output {
|
||||
let alertMessage = input.itemSelected.map { (model) -> String in
|
||||
let alertMessage = input.itemSelected.map { model -> String in
|
||||
let message = model.message
|
||||
|
||||
var copyContent:String = ""
|
||||
var copyContent: String = ""
|
||||
if let title = message.title {
|
||||
copyContent += "\(title)\n"
|
||||
}
|
||||
@ -97,45 +96,45 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
let refreshAction = BehaviorRelay<MJRefreshAction>(value: .none)
|
||||
// 切换群组
|
||||
let filterGroups: BehaviorRelay<[String?]> = {
|
||||
if let groups:[String?] = Settings["me.fin.filterGroups"] {
|
||||
if let groups: [String?] = Settings["me.fin.filterGroups"] {
|
||||
return BehaviorRelay<[String?]>(value: groups)
|
||||
}
|
||||
return BehaviorRelay<[String?]>(value: [])
|
||||
}()
|
||||
|
||||
// Message 转 MessageSection
|
||||
func messagesToMessageSection(messages:[Message]) -> [MessageSection] {
|
||||
let cellViewModels = messages.map({ (message) -> MessageTableViewCellViewModel in
|
||||
return MessageTableViewCellViewModel(message: message)
|
||||
})
|
||||
func messagesToMessageSection(messages: [Message]) -> [MessageSection] {
|
||||
let cellViewModels = messages.map { message -> MessageTableViewCellViewModel in
|
||||
MessageTableViewCellViewModel(message: message)
|
||||
}
|
||||
return [MessageSection(header: "model", messages: cellViewModels)]
|
||||
}
|
||||
//切换分组时,更新分组名
|
||||
// 切换分组时,更新分组名
|
||||
filterGroups
|
||||
.subscribe(onNext: {filterGroups in
|
||||
.subscribe(onNext: { filterGroups in
|
||||
if filterGroups.count <= 0 {
|
||||
titleRelay.accept(NSLocalizedString("historyMessage"))
|
||||
}
|
||||
else{
|
||||
else {
|
||||
titleRelay.accept(filterGroups.map { $0 ?? NSLocalizedString("default") }.joined(separator: " , "))
|
||||
}
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//切换分组和更改搜索词时,更新数据源
|
||||
// 切换分组和更改搜索词时,更新数据源
|
||||
Observable
|
||||
.combineLatest(filterGroups, input.searchText)
|
||||
.subscribe(onNext: {[weak self] groups, searchText in
|
||||
.subscribe(onNext: { [weak self] groups, searchText in
|
||||
self?.results = self?.getResults(filterGroups: groups, searchText: searchText)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//切换分组和下拉刷新时,重新刷新列表
|
||||
// 切换分组和下拉刷新时,重新刷新列表
|
||||
Observable
|
||||
.merge(
|
||||
input.refresh.asObservable().map{ () },
|
||||
filterGroups.map{ _ in () },
|
||||
input.searchText.asObservable().map{ _ in () }
|
||||
input.refresh.asObservable().map { () },
|
||||
filterGroups.map { _ in () },
|
||||
input.searchText.asObservable().map { _ in () }
|
||||
)
|
||||
.subscribe(onNext: {[weak self] in
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
strongSelf.page = 0
|
||||
messagesRelay.accept(
|
||||
@ -146,27 +145,27 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
refreshAction.accept(.endRefresh)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//加载更多
|
||||
// 加载更多
|
||||
input.loadMore.asObservable()
|
||||
.subscribe(onNext: {[weak self] in
|
||||
.subscribe(onNext: { [weak self] in
|
||||
guard let strongSelf = self else { return }
|
||||
let messages = strongSelf.getNextPage()
|
||||
let cellViewModels = messages.map({ (message) -> MessageTableViewCellViewModel in
|
||||
return MessageTableViewCellViewModel(message: message)
|
||||
})
|
||||
let cellViewModels = messages.map { message -> MessageTableViewCellViewModel in
|
||||
MessageTableViewCellViewModel(message: message)
|
||||
}
|
||||
|
||||
refreshAction.accept(.endLoadmore)
|
||||
if var section = messagesRelay.value.first {
|
||||
section.messages.append(contentsOf: cellViewModels)
|
||||
messagesRelay.accept([section])
|
||||
}
|
||||
else{
|
||||
else {
|
||||
messagesRelay.accept([MessageSection(header: "model", messages: cellViewModels)])
|
||||
}
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//删除message
|
||||
input.itemDelete.drive(onNext: {[weak self] indexPath in
|
||||
// 删除message
|
||||
input.itemDelete.drive(onNext: { [weak self] indexPath in
|
||||
if var section = messagesRelay.value.first {
|
||||
if let realm = try? Realm() {
|
||||
try? realm.write {
|
||||
@ -180,19 +179,19 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
// cell 中点击 url。
|
||||
let urlTap = messagesRelay.flatMapLatest { (section) -> Observable<String> in
|
||||
let urlTap = messagesRelay.flatMapLatest { section -> Observable<String> in
|
||||
if let section = section.first {
|
||||
let taps = section.messages.compactMap { (model) -> Observable<String> in
|
||||
return model.urlTap.asObservable()
|
||||
let taps = section.messages.compactMap { model -> Observable<String> in
|
||||
model.urlTap.asObservable()
|
||||
}
|
||||
return Observable.merge(taps)
|
||||
}
|
||||
return .empty()
|
||||
}
|
||||
.compactMap { URL(string: $0) } //只处理正确的url
|
||||
.compactMap { URL(string: $0) } // 只处理正确的url
|
||||
|
||||
//批量删除
|
||||
input.delete.drive(onNext: {[weak self] type in
|
||||
// 批量删除
|
||||
input.delete.drive(onNext: { [weak self] type in
|
||||
guard let strongSelf = self else { return }
|
||||
|
||||
var date = Date()
|
||||
@ -211,7 +210,7 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
guard let messages = strongSelf.getResults(filterGroups: filterGroups.value, searchText: nil)?.filter("createDate >= %@", date) else {
|
||||
return
|
||||
}
|
||||
try? realm.write{
|
||||
try? realm.write {
|
||||
for msg in messages {
|
||||
msg.isDeleted = true
|
||||
}
|
||||
@ -223,31 +222,31 @@ class MessageListViewModel: ViewModel,ViewModelType {
|
||||
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//群组筛选
|
||||
let groupFilter = input.groupTap.compactMap {() -> GroupFilterViewModel? in
|
||||
// 群组筛选
|
||||
let groupFilter = input.groupTap.compactMap { () -> GroupFilterViewModel? in
|
||||
if let realm = try? Realm() {
|
||||
let groups = realm.objects(Message.self)
|
||||
.filter("isDeleted != true")
|
||||
.distinct(by: ["group"])
|
||||
.value(forKeyPath: "group") as? [String?]
|
||||
|
||||
let groupModels = groups?.compactMap({ groupName -> GroupFilterModel in
|
||||
let groupModels = groups?.compactMap { groupName -> GroupFilterModel in
|
||||
var check = true
|
||||
if filterGroups.value.count > 0 {
|
||||
check = filterGroups.value.contains(groupName)
|
||||
}
|
||||
return GroupFilterModel(name: groupName, checked: check)
|
||||
})
|
||||
}
|
||||
|
||||
if let models = groupModels {
|
||||
let viewModel = GroupFilterViewModel(groups: models)
|
||||
|
||||
//保存选择的 group
|
||||
// 保存选择的 group
|
||||
viewModel.done.subscribe(onNext: { filterGroups in
|
||||
Settings["me.fin.filterGroups"] = filterGroups
|
||||
}).disposed(by: viewModel.rx.disposeBag)
|
||||
|
||||
//将选择绑定到当前页面
|
||||
// 将选择绑定到当前页面
|
||||
viewModel.done
|
||||
.bind(to: filterGroups)
|
||||
.disposed(by: viewModel.rx.disposeBag)
|
||||
|
||||
@ -6,9 +6,9 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import RxDataSources
|
||||
import UIKit
|
||||
class MessageSettingsViewController: BaseViewController {
|
||||
let tableView: UITableView = {
|
||||
let tableView = UITableView()
|
||||
@ -22,14 +22,16 @@ class MessageSettingsViewController: BaseViewController {
|
||||
|
||||
return tableView
|
||||
}()
|
||||
|
||||
override func makeUI() {
|
||||
self.title = NSLocalizedString("settings")
|
||||
|
||||
self.view.addSubview(tableView)
|
||||
tableView.snp.makeConstraints { (make) in
|
||||
tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
override func bindViewModel() {
|
||||
guard let viewModel = self.viewModel as? MessageSettingsViewModel else {
|
||||
return
|
||||
@ -40,9 +42,9 @@ class MessageSettingsViewController: BaseViewController {
|
||||
)
|
||||
)
|
||||
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, MessageSettingItem>> { (source, tableView, indexPath, item) -> UITableViewCell in
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, MessageSettingItem>> { _, tableView, _, item -> UITableViewCell in
|
||||
switch item {
|
||||
case .label(let text):
|
||||
case let .label(text):
|
||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "\(LabelCell.self)") as? LabelCell {
|
||||
cell.textLabel?.text = text
|
||||
return cell
|
||||
@ -51,12 +53,12 @@ class MessageSettingsViewController: BaseViewController {
|
||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "\(iCloudStatusCell.self)") {
|
||||
return cell
|
||||
}
|
||||
case .archiveSetting(let viewModel):
|
||||
case let .archiveSetting(viewModel):
|
||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "\(ArchiveSettingCell.self)") as? ArchiveSettingCell {
|
||||
cell.bindViewModel(model: viewModel)
|
||||
return cell
|
||||
}
|
||||
case let .detail(title,text,textColor,_):
|
||||
case let .detail(title, text, textColor, _):
|
||||
if let cell = tableView.dequeueReusableCell(withIdentifier: "\(DetailTextCell.self)") as? DetailTextCell {
|
||||
cell.textLabel?.text = title
|
||||
cell.detailTextLabel?.text = text
|
||||
@ -78,10 +80,8 @@ class MessageSettingsViewController: BaseViewController {
|
||||
.drive(tableView.rx.items(dataSource: dataSource))
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
output.openUrl.drive {[weak self] url in
|
||||
output.openUrl.drive { [weak self] url in
|
||||
self?.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)
|
||||
}.disposed(by: rx.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,22 +7,23 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import Material
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import Material
|
||||
import RxSwift
|
||||
|
||||
class MessageSettingsViewModel: ViewModel, ViewModelType {
|
||||
struct Input {
|
||||
var itemSelected: Driver<MessageSettingItem>
|
||||
}
|
||||
|
||||
struct Output {
|
||||
var settings:Driver<[SectionModel<String, MessageSettingItem>]>
|
||||
var openUrl:Driver<URL>
|
||||
var settings: Driver<[SectionModel<String, MessageSettingItem>]>
|
||||
var openUrl: Driver<URL>
|
||||
}
|
||||
|
||||
func transform(input: Input) -> Output {
|
||||
|
||||
let settings:[MessageSettingItem] = {
|
||||
let settings: [MessageSettingItem] = {
|
||||
var settings = [MessageSettingItem]()
|
||||
settings.append(.label(text: "iCloud"))
|
||||
settings.append(.iCloudStatus)
|
||||
@ -32,41 +33,41 @@ class MessageSettingsViewModel: ViewModel, ViewModelType {
|
||||
settings.append(.label(text: NSLocalizedString("archiveNote")))
|
||||
|
||||
if let infoDict = Bundle.main.infoDictionary,
|
||||
let runId = infoDict["GitHub Run Id"] as? String{
|
||||
let runId = infoDict["GitHub Run Id"] as? String
|
||||
{
|
||||
settings.append(.label(text: NSLocalizedString("buildInfo")))
|
||||
settings.append(.detail(
|
||||
title: "Github Run Id",
|
||||
text:"\(runId)",
|
||||
textColor: Color.grey.darken2,
|
||||
url: URL(string: "https://github.com/Finb/Bark/actions/runs/\(runId)")))
|
||||
title: "Github Run Id",
|
||||
text: "\(runId)",
|
||||
textColor: Color.grey.darken2,
|
||||
url: URL(string: "https://github.com/Finb/Bark/actions/runs/\(runId)")))
|
||||
settings.append(.label(text: NSLocalizedString("buildDesc")))
|
||||
}
|
||||
|
||||
|
||||
settings.append(.label(text: NSLocalizedString("other")))
|
||||
settings.append(.detail(
|
||||
title: NSLocalizedString("faq"),
|
||||
text: nil,
|
||||
textColor: nil,
|
||||
url: URL(string: "https://day.app/2021/06/barkfaq/")))
|
||||
title: NSLocalizedString("faq"),
|
||||
text: nil,
|
||||
textColor: nil,
|
||||
url: URL(string: "https://day.app/2021/06/barkfaq/")))
|
||||
|
||||
settings.append(.spacer(height: 0.5, color: Color.grey.lighten4))
|
||||
settings.append(.detail(
|
||||
title: NSLocalizedString("appSC"),
|
||||
text: nil,
|
||||
textColor: nil,
|
||||
url: URL(string: "https://github.com/Finb/Bark")))
|
||||
title: NSLocalizedString("appSC"),
|
||||
text: nil,
|
||||
textColor: nil,
|
||||
url: URL(string: "https://github.com/Finb/Bark")))
|
||||
|
||||
settings.append(.spacer(height: 0.5, color: Color.grey.lighten4))
|
||||
settings.append(.detail(
|
||||
title: NSLocalizedString("backendSC"),
|
||||
text: nil,
|
||||
textColor: nil,
|
||||
url: URL(string: "https://github.com/Finb/bark-server")))
|
||||
title: NSLocalizedString("backendSC"),
|
||||
text: nil,
|
||||
textColor: nil,
|
||||
url: URL(string: "https://github.com/Finb/bark-server")))
|
||||
return settings
|
||||
}()
|
||||
|
||||
settings.compactMap { (item) -> ArchiveSettingCellViewModel? in
|
||||
settings.compactMap { item -> ArchiveSettingCellViewModel? in
|
||||
if case let MessageSettingItem.archiveSetting(viewModel) = item {
|
||||
return viewModel
|
||||
}
|
||||
@ -74,7 +75,7 @@ class MessageSettingsViewModel: ViewModel, ViewModelType {
|
||||
}
|
||||
.first?
|
||||
.on
|
||||
.subscribe(onNext: { (on) in
|
||||
.subscribe(onNext: { on in
|
||||
ArchiveSettingManager.shared.isArchive = on
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
@ -85,25 +86,22 @@ class MessageSettingsViewModel: ViewModel, ViewModelType {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
return Output(
|
||||
settings: Driver<[SectionModel<String, MessageSettingItem>]>
|
||||
.just([SectionModel(model: "model", items: settings)]),
|
||||
openUrl: openUrl
|
||||
)
|
||||
.just([SectionModel(model: "model", items: settings)]),
|
||||
openUrl: openUrl)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
enum MessageSettingItem {
|
||||
// 普通标题标签
|
||||
case label(text:String)
|
||||
case label(text: String)
|
||||
// iCloud 状态
|
||||
case iCloudStatus
|
||||
// 默认保存
|
||||
case archiveSetting(viewModel:ArchiveSettingCellViewModel)
|
||||
case archiveSetting(viewModel: ArchiveSettingCellViewModel)
|
||||
// 带 详细按钮的 文本cell
|
||||
case detail(title:String?, text:String?, textColor:UIColor?, url:URL?)
|
||||
case detail(title: String?, text: String?, textColor: UIColor?, url: URL?)
|
||||
// 分隔线
|
||||
case spacer(height:CGFloat, color:UIColor?)
|
||||
case spacer(height: CGFloat, color: UIColor?)
|
||||
}
|
||||
|
||||
@ -6,22 +6,21 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import SnapKit
|
||||
import SafariServices
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
import SafariServices
|
||||
import SnapKit
|
||||
import UIKit
|
||||
|
||||
class NewServerViewController: BaseViewController {
|
||||
|
||||
let addressTextField : TextField = {
|
||||
let addressTextField: TextField = {
|
||||
let textField = TextField()
|
||||
textField.keyboardType = .URL
|
||||
textField.placeholder = NSLocalizedString("ServerAddress")
|
||||
textField.detail = NSLocalizedString("ServerExample")
|
||||
textField.transition([ .scale(0.85) , .opacity(0)] )
|
||||
textField.detailLabel.transition([ .scale(0.85) , .opacity(0)] )
|
||||
textField.transition([.scale(0.85), .opacity(0)])
|
||||
textField.detailLabel.transition([.scale(0.85), .opacity(0)])
|
||||
return textField
|
||||
}()
|
||||
|
||||
@ -30,7 +29,7 @@ class NewServerViewController: BaseViewController {
|
||||
label.text = NSLocalizedString("DeploymentDocuments")
|
||||
label.textColor = Color.blue.base
|
||||
label.font = UIFont.systemFont(ofSize: 12)
|
||||
label.transition([ .scale(0.85) , .opacity(0), .translate(x: 50)] )
|
||||
label.transition([.scale(0.85), .opacity(0), .translate(x: 50)])
|
||||
label.isUserInteractionEnabled = true
|
||||
label.addGestureRecognizer(UITapGestureRecognizer())
|
||||
return label
|
||||
@ -52,35 +51,34 @@ class NewServerViewController: BaseViewController {
|
||||
.top(kNavigationHeight + 40).left(10).right(10)
|
||||
|
||||
self.view.addSubview(noticeLabel)
|
||||
noticeLabel.snp.makeConstraints { (make) in
|
||||
noticeLabel.snp.makeConstraints { make in
|
||||
make.top.equalTo(self.addressTextField.snp.bottom).offset(40)
|
||||
make.left.equalTo(self.addressTextField)
|
||||
}
|
||||
}
|
||||
|
||||
override func bindViewModel() {
|
||||
guard let viewModel = self.viewModel as? NewServerViewModel else {
|
||||
return
|
||||
}
|
||||
let noticeTap = noticeLabel.gestureRecognizers!.first!.rx
|
||||
.event
|
||||
.map({ (_) -> () in
|
||||
return ()
|
||||
})
|
||||
.map { _ -> () in
|
||||
()
|
||||
}
|
||||
.asDriver(onErrorJustReturn: ())
|
||||
|
||||
let done = doneButton.rx.tap
|
||||
.map({[weak self] in
|
||||
return self?.addressTextField.text ?? ""
|
||||
})
|
||||
.map { [weak self] in
|
||||
self?.addressTextField.text ?? ""
|
||||
}
|
||||
.asDriver(onErrorDriveWith: .empty())
|
||||
|
||||
let viewDidAppear = rx
|
||||
.methodInvoked(#selector(viewDidAppear(_:)))
|
||||
.map{ _ in () }
|
||||
.map { _ in () }
|
||||
.asDriver(onErrorDriveWith: .empty())
|
||||
|
||||
|
||||
|
||||
let output = viewModel.transform(
|
||||
input: NewServerViewModel.Input(
|
||||
noticeClick: noticeTap,
|
||||
@ -88,36 +86,34 @@ class NewServerViewController: BaseViewController {
|
||||
viewDidAppear: viewDidAppear
|
||||
))
|
||||
|
||||
//键盘显示与隐藏
|
||||
// 键盘显示与隐藏
|
||||
output.showKeyboard.drive(onNext: { [weak self] show in
|
||||
if show {
|
||||
_ = self?.addressTextField.becomeFirstResponder()
|
||||
}
|
||||
else{
|
||||
else {
|
||||
self?.addressTextField.resignFirstResponder()
|
||||
}
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//点击教程
|
||||
output.notice.drive(onNext: {[weak self] url in
|
||||
// 点击教程
|
||||
output.notice.drive(onNext: { [weak self] url in
|
||||
self?.navigationController?.present(BarkSFSafariViewController(url: url), animated: true, completion: nil)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//URL文本框文本
|
||||
// URL文本框文本
|
||||
output.urlText
|
||||
.drive(self.addressTextField.rx.text)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
//退出页面
|
||||
output.pop.drive(onNext: {[weak self] _ in
|
||||
// 退出页面
|
||||
output.pop.drive(onNext: { [weak self] _ in
|
||||
self?.navigationController?.popViewController(animated: true)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
//弹出提示文本
|
||||
output.showSnackbar.drive(onNext: {[weak self] text in
|
||||
// 弹出提示文本
|
||||
output.showSnackbar.drive(onNext: { [weak self] text in
|
||||
self?.showSnackbar(text: text)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -7,14 +7,14 @@
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import SwiftyJSON
|
||||
import Moya
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
import SwiftyJSON
|
||||
|
||||
class NewServerViewModel: ViewModel, ViewModelType {
|
||||
struct Input {
|
||||
var noticeClick:Driver<Void>
|
||||
var noticeClick: Driver<Void>
|
||||
var done: Driver<String>
|
||||
var viewDidAppear: Driver<Void>
|
||||
}
|
||||
@ -27,23 +27,21 @@ class NewServerViewModel: ViewModel, ViewModelType {
|
||||
var pop: Driver<String>
|
||||
}
|
||||
|
||||
|
||||
private var url: String = ""
|
||||
|
||||
let pop = PublishRelay<String>()
|
||||
|
||||
func transform(input: Input) -> Output {
|
||||
|
||||
let showKeyboard = PublishRelay<Bool>()
|
||||
let urlText = PublishRelay<String>()
|
||||
let showSnackbar = PublishRelay<String>()
|
||||
|
||||
let notice = input.noticeClick
|
||||
.map{ URL(string: "https://day.app/2018/06/bark-server-document/")! }
|
||||
.map { URL(string: "https://day.app/2018/06/bark-server-document/")! }
|
||||
.asDriver()
|
||||
|
||||
input.viewDidAppear
|
||||
.map{ "https://" }
|
||||
.map { "https://" }
|
||||
.asObservable()
|
||||
.take(1)
|
||||
.subscribe(onNext: { text in
|
||||
@ -53,7 +51,7 @@ class NewServerViewModel: ViewModel, ViewModelType {
|
||||
|
||||
input.done
|
||||
.asObservable()
|
||||
.flatMapLatest {[weak self] (url) -> Observable<Result<JSON,ApiError>> in
|
||||
.flatMapLatest { [weak self] url -> Observable<Result<JSON, ApiError>> in
|
||||
showKeyboard.accept(false)
|
||||
if let _ = URL(string: url) {
|
||||
guard let strongSelf = self else { return .empty() }
|
||||
@ -67,7 +65,7 @@ class NewServerViewModel: ViewModel, ViewModelType {
|
||||
return .empty()
|
||||
}
|
||||
}
|
||||
.subscribe(onNext: {[weak self] response in
|
||||
.subscribe(onNext: { [weak self] response in
|
||||
guard let strongSelf = self else { return }
|
||||
switch response {
|
||||
case .success:
|
||||
|
||||
@ -6,14 +6,14 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import AVKit
|
||||
import Material
|
||||
import UIKit
|
||||
|
||||
import RxSwift
|
||||
import NSObject_Rx
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import NSObject_Rx
|
||||
import RxSwift
|
||||
|
||||
class SoundsViewController: BaseViewController {
|
||||
let tableView: UITableView = {
|
||||
@ -22,12 +22,12 @@ class SoundsViewController: BaseViewController {
|
||||
tableView.register(SoundCell.self, forCellReuseIdentifier: "\(SoundCell.self)")
|
||||
return tableView
|
||||
}()
|
||||
|
||||
|
||||
override func makeUI() {
|
||||
self.title = NSLocalizedString("notificationSound")
|
||||
|
||||
self.view.addSubview(self.tableView)
|
||||
self.tableView.snp.makeConstraints { (make) in
|
||||
self.tableView.snp.makeConstraints { make in
|
||||
make.edges.equalToSuperview()
|
||||
}
|
||||
|
||||
@ -40,35 +40,36 @@ class SoundsViewController: BaseViewController {
|
||||
return header
|
||||
}()
|
||||
}
|
||||
|
||||
override func bindViewModel() {
|
||||
guard let viewModel = viewModel as? SoundsViewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
let output = viewModel.transform(
|
||||
input: SoundsViewModel.Input(soundSelected: self.tableView.rx
|
||||
.modelSelected(SoundCellViewModel.self)
|
||||
.asDriver()))
|
||||
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String,SoundCellViewModel>> { (source, tableView, indexPath, item) -> UITableViewCell in
|
||||
.modelSelected(SoundCellViewModel.self)
|
||||
.asDriver()))
|
||||
|
||||
let dataSource = RxTableViewSectionedReloadDataSource<SectionModel<String, SoundCellViewModel>> { _, tableView, _, item -> UITableViewCell in
|
||||
guard let cell = tableView.dequeueReusableCell(withIdentifier: "\(SoundCell.self)") as? SoundCell else {
|
||||
return UITableViewCell()
|
||||
}
|
||||
cell.bindViewModel(model: item)
|
||||
return cell
|
||||
}
|
||||
|
||||
|
||||
output.audios
|
||||
.bind(to: tableView.rx.items(dataSource: dataSource))
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
|
||||
output.copyNameAction.drive(onNext: { name in
|
||||
UIPasteboard.general.string = name.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
self.navigationController?.showSnackbar(text: NSLocalizedString("Copy"))
|
||||
}).disposed(by: rx.disposeBag)
|
||||
|
||||
|
||||
output.playAction.drive(onNext: { url in
|
||||
var soundID:SystemSoundID = 0
|
||||
var soundID: SystemSoundID = 0
|
||||
AudioServicesCreateSystemSoundID(url, &soundID)
|
||||
AudioServicesPlaySystemSoundWithCompletion(soundID) {
|
||||
AudioServicesDisposeSystemSoundID(soundID)
|
||||
|
||||
@ -6,19 +6,19 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import AVKit
|
||||
import Foundation
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import RxSwift
|
||||
|
||||
class SoundsViewModel:ViewModel,ViewModelType {
|
||||
|
||||
class SoundsViewModel: ViewModel, ViewModelType {
|
||||
struct Input {
|
||||
var soundSelected:Driver<SoundCellViewModel>
|
||||
var soundSelected: Driver<SoundCellViewModel>
|
||||
}
|
||||
|
||||
struct Output {
|
||||
var audios:Observable<[ SectionModel<String, SoundCellViewModel>]>
|
||||
var audios: Observable<[SectionModel<String, SoundCellViewModel>]>
|
||||
var copyNameAction: Driver<String>
|
||||
var playAction: Driver<CFURL>
|
||||
}
|
||||
@ -26,15 +26,15 @@ class SoundsViewModel:ViewModel,ViewModelType {
|
||||
func transform(input: Input) -> Output {
|
||||
let models = { () -> [AVURLAsset] in
|
||||
var urls = Bundle.main.urls(forResourcesWithExtension: "caf", subdirectory: nil) ?? []
|
||||
urls.sort { (u1, u2) -> Bool in
|
||||
urls.sort { u1, u2 -> Bool in
|
||||
u1.lastPathComponent.localizedStandardCompare(u2.lastPathComponent) == ComparisonResult.orderedAscending
|
||||
}
|
||||
let audios = urls.map { (url) -> AVURLAsset in
|
||||
let audios = urls.map { url -> AVURLAsset in
|
||||
let asset = AVURLAsset(url: url)
|
||||
return asset
|
||||
}
|
||||
return audios
|
||||
}().map { SoundCellViewModel(model: $0 ) }
|
||||
}().map { SoundCellViewModel(model: $0) }
|
||||
|
||||
let copyAction = Driver.merge(
|
||||
models.map { $0.copyNameAction.asDriver(onErrorDriveWith: .empty()) }
|
||||
@ -43,8 +43,7 @@ class SoundsViewModel:ViewModel,ViewModelType {
|
||||
return Output(
|
||||
audios: Observable.just([SectionModel(model: "model", items: models)]),
|
||||
copyNameAction: copyAction,
|
||||
playAction: input.soundSelected.map{ $0.model.url as CFURL }
|
||||
playAction: input.soundSelected.map { $0.model.url as CFURL }
|
||||
)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -18,7 +18,6 @@ import Intents
|
||||
// "Search for messages in <myApp>"
|
||||
|
||||
class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessagesIntentHandling, INSetMessageAttributeIntentHandling {
|
||||
|
||||
override func handler(for intent: INIntent) -> Any {
|
||||
// This is the default implementation. If you want different objects to handle different intents,
|
||||
// you can override this and return the handler you want for that particular intent.
|
||||
@ -31,7 +30,6 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
// Implement resolution methods to provide additional information about your intent (optional).
|
||||
func resolveRecipients(for intent: INSendMessageIntent, with completion: @escaping ([INSendMessageRecipientResolutionResult]) -> Void) {
|
||||
if let recipients = intent.recipients {
|
||||
|
||||
// If no recipients were provided we'll need to prompt for a value.
|
||||
if recipients.count == 0 {
|
||||
completion([INSendMessageRecipientResolutionResult.needsValue()])
|
||||
@ -42,7 +40,7 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
for recipient in recipients {
|
||||
let matchingContacts = [recipient] // Implement your contact matching logic here to create an array of matching contacts
|
||||
switch matchingContacts.count {
|
||||
case 2 ... Int.max:
|
||||
case 2 ... Int.max:
|
||||
// We need Siri's help to ask user to pick one from the matches.
|
||||
resolutionResults += [INSendMessageRecipientResolutionResult.disambiguation(with: matchingContacts)]
|
||||
|
||||
@ -56,7 +54,6 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
|
||||
default:
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
completion(resolutionResults)
|
||||
@ -107,9 +104,9 @@ class IntentHandler: INExtension, INSendMessageIntentHandling, INSearchForMessag
|
||||
identifier: "identifier",
|
||||
content: "I am so excited about SiriKit!",
|
||||
dateSent: Date(),
|
||||
sender: INPerson(personHandle: INPersonHandle(value: "sarah@example.com", type: .emailAddress), nameComponents: nil, displayName: "Sarah", image: nil, contactIdentifier: nil, customIdentifier: nil),
|
||||
recipients: [INPerson(personHandle: INPersonHandle(value: "+1-415-555-5555", type: .phoneNumber), nameComponents: nil, displayName: "John", image: nil, contactIdentifier: nil, customIdentifier: nil)]
|
||||
)]
|
||||
sender: INPerson(personHandle: INPersonHandle(value: "sarah@example.com", type: .emailAddress), nameComponents: nil, displayName: "Sarah", image: nil, contactIdentifier: nil, customIdentifier: nil),
|
||||
recipients: [INPerson(personHandle: INPersonHandle(value: "+1-415-555-5555", type: .phoneNumber), nameComponents: nil, displayName: "John", image: nil, contactIdentifier: nil, customIdentifier: nil)]
|
||||
)]
|
||||
completion(response)
|
||||
}
|
||||
|
||||
|
||||
@ -6,25 +6,26 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RealmSwift
|
||||
import IceCream
|
||||
import RealmSwift
|
||||
import UIKit
|
||||
class Message: Object {
|
||||
@objc dynamic var id = NSUUID().uuidString
|
||||
@objc dynamic var title:String?
|
||||
@objc dynamic var body:String?
|
||||
@objc dynamic var url:String?
|
||||
@objc dynamic var group:String?
|
||||
@objc dynamic var createDate:Date?
|
||||
|
||||
@objc dynamic var title: String?
|
||||
@objc dynamic var body: String?
|
||||
@objc dynamic var url: String?
|
||||
@objc dynamic var group: String?
|
||||
@objc dynamic var createDate: Date?
|
||||
|
||||
// 设置为 true 后,将被IceCream自动清理
|
||||
@objc dynamic var isDeleted = false
|
||||
|
||||
|
||||
override class func primaryKey() -> String? {
|
||||
return "id"
|
||||
}
|
||||
|
||||
override class func indexedProperties() -> [String] {
|
||||
return ["group","createDate"]
|
||||
return ["group", "createDate"]
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,24 +10,24 @@ import Foundation
|
||||
import UIKit
|
||||
|
||||
class PreviewModel: NSObject {
|
||||
var title:String?
|
||||
var body:String?
|
||||
var category:String?
|
||||
var notice:String?
|
||||
var queryParameter:String?
|
||||
var image:UIImage?
|
||||
var moreInfo:String?
|
||||
var moreViewModel:ViewModel?
|
||||
|
||||
init(title:String? = nil,
|
||||
body:String? = nil,
|
||||
category:String? = nil,
|
||||
notice:String? = nil,
|
||||
queryParameter:String? = nil,
|
||||
image:UIImage? = nil,
|
||||
moreInfo:String? = nil,
|
||||
moreViewModel:ViewModel? = nil
|
||||
) {
|
||||
var title: String?
|
||||
var body: String?
|
||||
var category: String?
|
||||
var notice: String?
|
||||
var queryParameter: String?
|
||||
var image: UIImage?
|
||||
var moreInfo: String?
|
||||
var moreViewModel: ViewModel?
|
||||
|
||||
init(title: String? = nil,
|
||||
body: String? = nil,
|
||||
category: String? = nil,
|
||||
notice: String? = nil,
|
||||
queryParameter: String? = nil,
|
||||
image: UIImage? = nil,
|
||||
moreInfo: String? = nil,
|
||||
moreViewModel: ViewModel? = nil)
|
||||
{
|
||||
self.title = title
|
||||
self.body = body
|
||||
self.category = category
|
||||
|
||||
@ -6,75 +6,74 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
import RealmSwift
|
||||
import Intents
|
||||
import Kingfisher
|
||||
import MobileCoreServices
|
||||
import Intents
|
||||
import RealmSwift
|
||||
import UIKit
|
||||
import UserNotifications
|
||||
class NotificationService: UNNotificationServiceExtension {
|
||||
var contentHandler: ((UNNotificationContent) -> ())?
|
||||
|
||||
var contentHandler: ((UNNotificationContent) -> Void)?
|
||||
|
||||
lazy var realm:Realm? = {
|
||||
lazy var realm: Realm? = {
|
||||
let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark")
|
||||
let fileUrl = groupUrl?.appendingPathComponent("bark.realm")
|
||||
let config = Realm.Configuration(
|
||||
fileURL: fileUrl,
|
||||
schemaVersion: 13,
|
||||
migrationBlock: { migration, oldSchemaVersion in
|
||||
migrationBlock: { _, oldSchemaVersion in
|
||||
// We haven’t migrated anything yet, so oldSchemaVersion == 0
|
||||
if (oldSchemaVersion < 1) {
|
||||
if oldSchemaVersion < 1 {
|
||||
// Nothing to do!
|
||||
// Realm will automatically detect new properties and removed properties
|
||||
// And will update the schema on disk automatically
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
// Tell Realm to use this new configuration object for the default Realm
|
||||
Realm.Configuration.defaultConfiguration = config
|
||||
return try? Realm()
|
||||
}()
|
||||
|
||||
|
||||
/// 自动保存推送
|
||||
/// - Parameters:
|
||||
/// - userInfo: 推送参数
|
||||
/// - bestAttemptContentBody: 推送body,如果用户`没有指定要复制的值` ,默认复制 `推送正文`
|
||||
fileprivate func autoCopy(_ userInfo: [AnyHashable : Any], defaultCopy: String) {
|
||||
fileprivate func autoCopy(_ userInfo: [AnyHashable: Any], defaultCopy: String) {
|
||||
if userInfo["autocopy"] as? String == "1"
|
||||
|| userInfo["automaticallycopy"] as? String == "1"{
|
||||
|| userInfo["automaticallycopy"] as? String == "1"
|
||||
{
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
}
|
||||
else{
|
||||
else {
|
||||
UIPasteboard.general.string = defaultCopy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 保存推送
|
||||
/// - Parameter userInfo: 推送参数
|
||||
/// 如果用户携带了 `isarchive` 参数,则以 `isarchive` 参数值为准
|
||||
/// 否则,以用户`应用内设置`为准
|
||||
fileprivate func archive(_ userInfo: [AnyHashable : Any]) {
|
||||
var isArchive:Bool?
|
||||
if let archive = userInfo["isarchive"] as? String{
|
||||
fileprivate func archive(_ userInfo: [AnyHashable: Any]) {
|
||||
var isArchive: Bool?
|
||||
if let archive = userInfo["isarchive"] as? String {
|
||||
isArchive = archive == "1" ? true : false
|
||||
}
|
||||
if isArchive == nil {
|
||||
isArchive = ArchiveSettingManager.shared.isArchive
|
||||
}
|
||||
let alert = (userInfo["aps"] as? [String:Any])?["alert"] as? [String:Any]
|
||||
let alert = (userInfo["aps"] as? [String: Any])?["alert"] as? [String: Any]
|
||||
let title = alert?["title"] as? String
|
||||
let body = alert?["body"] as? String
|
||||
|
||||
let url = userInfo["url"] as? String
|
||||
let group = userInfo["group"] as? String
|
||||
|
||||
if (isArchive == true){
|
||||
try? realm?.write{
|
||||
if isArchive == true {
|
||||
try? realm?.write {
|
||||
let message = Message()
|
||||
message.title = title
|
||||
message.body = body
|
||||
@ -86,22 +85,21 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// 下载推送图片
|
||||
/// - Parameters:
|
||||
/// - userInfo: 推送参数
|
||||
/// - bestAttemptContent: 推送content
|
||||
/// - complection: 下载图片完毕后的回调函数
|
||||
fileprivate func downloadImage(_ imageUrl:String, complection: @escaping (_ imageFileUrl:String?) -> () ) {
|
||||
fileprivate func downloadImage(_ imageUrl: String, complection: @escaping (_ imageFileUrl: String?) -> ()) {
|
||||
guard let groupUrl = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.bark"),
|
||||
let cache = try? ImageCache(name: "shared",cacheDirectoryURL: groupUrl),
|
||||
let cache = try? ImageCache(name: "shared", cacheDirectoryURL: groupUrl),
|
||||
let imageResource = URL(string: imageUrl)
|
||||
else {
|
||||
complection(nil)
|
||||
return
|
||||
}
|
||||
|
||||
func downloadFinished(){
|
||||
func downloadFinished() {
|
||||
let cacheFileUrl = cache.cachePath(forKey: imageResource.cacheKey)
|
||||
complection(cacheFileUrl)
|
||||
}
|
||||
@ -119,7 +117,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
// 缓存图片
|
||||
cache.storeToDisk(result.originalData, forKey: imageResource.cacheKey, expiration: StorageExpiration.never) { r in
|
||||
cache.storeToDisk(result.originalData, forKey: imageResource.cacheKey, expiration: StorageExpiration.never) { _ in
|
||||
downloadFinished()
|
||||
}
|
||||
}
|
||||
@ -130,14 +128,15 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
/// - bestAttemptContent: 推送content
|
||||
/// - complection: 下载图片完毕后的回调函数
|
||||
fileprivate func setImage(content bestAttemptContent: UNMutableNotificationContent,
|
||||
complection: @escaping (_ content:UNMutableNotificationContent) -> () ) {
|
||||
complection: @escaping (_ content: UNMutableNotificationContent) -> ())
|
||||
{
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
guard let imageUrl = userInfo["image"] as? String else {
|
||||
complection(bestAttemptContent)
|
||||
return
|
||||
}
|
||||
|
||||
func finished(_ imageFileUrl: String?){
|
||||
func finished(_ imageFileUrl: String?) {
|
||||
guard let imageFileUrl = imageFileUrl else {
|
||||
complection(bestAttemptContent)
|
||||
return
|
||||
@ -146,13 +145,15 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
// 将图片缓存复制一份,推送使用完后会自动删除,但图片缓存需要留着以后在历史记录里查看
|
||||
try? FileManager.default.copyItem(
|
||||
at: URL(fileURLWithPath: imageFileUrl),
|
||||
to: copyDestUrl)
|
||||
to: copyDestUrl
|
||||
)
|
||||
|
||||
if let attachment = try? UNNotificationAttachment(
|
||||
if let attachment = try? UNNotificationAttachment(
|
||||
identifier: "image",
|
||||
url: copyDestUrl,
|
||||
options: [UNNotificationAttachmentOptionsTypeHintKey : kUTTypePNG]){
|
||||
bestAttemptContent.attachments = [ attachment ]
|
||||
options: [UNNotificationAttachmentOptionsTypeHintKey: kUTTypePNG]
|
||||
) {
|
||||
bestAttemptContent.attachments = [attachment]
|
||||
}
|
||||
complection(bestAttemptContent)
|
||||
}
|
||||
@ -165,7 +166,8 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
/// - bestAttemptContent: 推送 content
|
||||
/// - complection: 设置完成后的回调参数
|
||||
fileprivate func setIcon(content bestAttemptContent: UNMutableNotificationContent,
|
||||
complection: @escaping (_ content:UNMutableNotificationContent) -> () ) {
|
||||
complection: @escaping (_ content: UNMutableNotificationContent) -> ())
|
||||
{
|
||||
if #available(iOSApplicationExtension 15.0, *) {
|
||||
let userInfo = bestAttemptContent.userInfo
|
||||
guard let imageUrl = userInfo["icon"] as? String else {
|
||||
@ -173,7 +175,7 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
return
|
||||
}
|
||||
|
||||
func finished(_ imageFileUrl: String?){
|
||||
func finished(_ imageFileUrl: String?) {
|
||||
guard let imageFileUrl = imageFileUrl else {
|
||||
complection(bestAttemptContent)
|
||||
return
|
||||
@ -224,7 +226,8 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
do {
|
||||
let content = try bestAttemptContent.updating(from: intent) as! UNMutableNotificationContent
|
||||
complection(content)
|
||||
} catch {
|
||||
}
|
||||
catch {
|
||||
// Handle error
|
||||
}
|
||||
|
||||
@ -233,13 +236,12 @@ class NotificationService: UNNotificationServiceExtension {
|
||||
|
||||
downloadImage(imageUrl, complection: finished)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
complection(bestAttemptContent)
|
||||
}
|
||||
}
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> Void) {
|
||||
|
||||
override func didReceive(_ request: UNNotificationRequest, withContentHandler contentHandler: @escaping (UNNotificationContent) -> ()) {
|
||||
self.contentHandler = contentHandler
|
||||
guard let bestAttemptContent = (request.content.mutableCopy() as? UNMutableNotificationContent) else {
|
||||
contentHandler(request.content)
|
||||
|
||||
@ -13,22 +13,25 @@ class ArchiveSettingCell: BaseTableViewCell {
|
||||
let btn = UISwitch()
|
||||
return btn
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
self.selectionStyle = .none
|
||||
|
||||
|
||||
self.textLabel?.text = NSLocalizedString("defaultArchiveSettings")
|
||||
|
||||
|
||||
contentView.addSubview(switchButton)
|
||||
switchButton.snp.makeConstraints { (make) in
|
||||
switchButton.snp.makeConstraints { make in
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
|
||||
override func bindViewModel(model: ViewModel) {
|
||||
super.bindViewModel(model: model)
|
||||
guard let viewModel = model as? ArchiveSettingCellViewModel else {
|
||||
@ -38,4 +41,3 @@ class ArchiveSettingCell: BaseTableViewCell {
|
||||
.disposed(by: rx.reuseBag)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -10,7 +10,7 @@ import Foundation
|
||||
import RxCocoa
|
||||
class ArchiveSettingCellViewModel: ViewModel {
|
||||
var on: BehaviorRelay<Bool>
|
||||
init(on:Bool) {
|
||||
init(on: Bool) {
|
||||
self.on = BehaviorRelay<Bool>(value: on)
|
||||
super.init()
|
||||
}
|
||||
|
||||
@ -8,25 +8,25 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
protocol AlignmentRectInsetsOverridable:AnyObject {
|
||||
var alignmentRectInsetsOverride: UIEdgeInsets? {get set}
|
||||
}
|
||||
protocol HitTestSlopable:AnyObject {
|
||||
var hitTestSlop: UIEdgeInsets {get set}
|
||||
protocol AlignmentRectInsetsOverridable: AnyObject {
|
||||
var alignmentRectInsetsOverride: UIEdgeInsets? { get set }
|
||||
}
|
||||
|
||||
class BKButton: UIButton, HitTestSlopable,AlignmentRectInsetsOverridable {
|
||||
|
||||
var hitTestSlop:UIEdgeInsets = UIEdgeInsets.zero
|
||||
protocol HitTestSlopable: AnyObject {
|
||||
var hitTestSlop: UIEdgeInsets { get set }
|
||||
}
|
||||
|
||||
class BKButton: UIButton, HitTestSlopable, AlignmentRectInsetsOverridable {
|
||||
var hitTestSlop = UIEdgeInsets.zero
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if hitTestSlop == UIEdgeInsets.zero {
|
||||
return super.point(inside: point, with:event)
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
return self.bounds.inset(by: hitTestSlop).contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
var alignmentRectInsetsOverride: UIEdgeInsets?
|
||||
override var alignmentRectInsets: UIEdgeInsets {
|
||||
return alignmentRectInsetsOverride ?? super.alignmentRectInsets
|
||||
|
||||
@ -9,16 +9,14 @@
|
||||
import UIKit
|
||||
|
||||
class BKLabel: UILabel {
|
||||
var hitTestSlop = UIEdgeInsets.zero
|
||||
|
||||
var hitTestSlop:UIEdgeInsets = UIEdgeInsets.zero
|
||||
|
||||
override func point(inside point: CGPoint, with event: UIEvent?) -> Bool {
|
||||
if hitTestSlop == UIEdgeInsets.zero {
|
||||
return super.point(inside: point, with:event)
|
||||
return super.point(inside: point, with: event)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
return self.bounds.inset(by: hitTestSlop).contains(point)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -9,8 +9,8 @@
|
||||
import UIKit
|
||||
|
||||
class BaseTableViewCell: UITableViewCell {
|
||||
var viewModel:ViewModel?
|
||||
func bindViewModel(model:ViewModel){
|
||||
var viewModel: ViewModel?
|
||||
func bindViewModel(model: ViewModel) {
|
||||
self.viewModel = model
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,16 +6,16 @@
|
||||
// Copyright © 2021 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import RxDataSources
|
||||
import RxSwift
|
||||
import UIKit
|
||||
|
||||
class GroupCellViewModel:ViewModel {
|
||||
class GroupCellViewModel: ViewModel {
|
||||
let name = BehaviorRelay<String?>(value: nil)
|
||||
let checked = BehaviorRelay<Bool>(value: false)
|
||||
|
||||
init(groupFilterModel:GroupFilterModel) {
|
||||
init(groupFilterModel: GroupFilterModel) {
|
||||
self.name.accept(groupFilterModel.name)
|
||||
self.checked.accept(groupFilterModel.checked)
|
||||
}
|
||||
|
||||
@ -6,16 +6,17 @@
|
||||
// Copyright © 2021 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import UIKit
|
||||
|
||||
class GroupTableViewCell: BaseTableViewCell {
|
||||
let nameLabel:UILabel = {
|
||||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.fontSize = 14
|
||||
label.textColor = Color.darkText.primary
|
||||
return label
|
||||
}()
|
||||
|
||||
let checkButton: BKButton = {
|
||||
let btn = BKButton()
|
||||
btn.setImage(UIImage(named: "baseline_radio_button_unchecked_black_24pt"), for: .normal)
|
||||
@ -37,31 +38,33 @@ class GroupTableViewCell: BaseTableViewCell {
|
||||
make.left.equalToSuperview().offset(15)
|
||||
make.centerY.equalToSuperview()
|
||||
}
|
||||
nameLabel.snp.makeConstraints { (make) in
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(checkButton.snp.right).offset(15)
|
||||
make.top.equalToSuperview().offset(15)
|
||||
make.bottom.equalToSuperview().offset(-15)
|
||||
}
|
||||
let tap = UITapGestureRecognizer()
|
||||
self.contentView.addGestureRecognizer(tap)
|
||||
tap.rx.event.subscribe(onNext: {[weak self] _ in
|
||||
tap.rx.event.subscribe(onNext: { [weak self] _ in
|
||||
(self?.viewModel as? GroupCellViewModel)?.checked.accept(!self!.checkButton.isSelected)
|
||||
}).disposed(by: rx.disposeBag)
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func bindViewModel(model:ViewModel){
|
||||
override func bindViewModel(model: ViewModel) {
|
||||
super.bindViewModel(model: model)
|
||||
guard let viewModel = model as? GroupCellViewModel else {
|
||||
return
|
||||
}
|
||||
|
||||
viewModel.name
|
||||
.map({ name in
|
||||
return name ?? NSLocalizedString("default")
|
||||
})
|
||||
.map { name in
|
||||
name ?? NSLocalizedString("default")
|
||||
}
|
||||
.bind(to: nameLabel.rx.text)
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
@ -70,10 +73,8 @@ class GroupTableViewCell: BaseTableViewCell {
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
viewModel.checked.subscribe(
|
||||
onNext: {[weak self] checked in
|
||||
onNext: { [weak self] checked in
|
||||
self?.checkButton.tintColor = checked ? Color.lightBlue.darken3 : Color.lightGray
|
||||
}).disposed(by: rx.reuseBag)
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,19 +6,21 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import UIKit
|
||||
|
||||
class LabelCell: UITableViewCell {
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
self.selectionStyle = .none
|
||||
|
||||
|
||||
self.backgroundColor = Color.grey.lighten5
|
||||
self.textLabel?.textColor = Color.darkText.secondary
|
||||
self.textLabel?.fontSize = 12
|
||||
self.textLabel?.numberOfLines = 0
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@ -6,10 +6,9 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import UIKit
|
||||
class MessageTableViewCell: BaseTableViewCell {
|
||||
|
||||
let backgroundPanel: UIView = {
|
||||
let view = UIView()
|
||||
view.layer.cornerRadius = 3
|
||||
@ -25,6 +24,7 @@ class MessageTableViewCell: BaseTableViewCell {
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let bodyLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = RobotoFont.regular(with: 14)
|
||||
@ -49,12 +49,14 @@ class MessageTableViewCell: BaseTableViewCell {
|
||||
label.textColor = Color.darkText.others
|
||||
return label
|
||||
}()
|
||||
|
||||
let bodyStackView: UIStackView = {
|
||||
let stackView = UIStackView()
|
||||
stackView.axis = .vertical
|
||||
return stackView
|
||||
}()
|
||||
let separatorLine:UIImageView = {
|
||||
|
||||
let separatorLine: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.backgroundColor = Color.grey.lighten5
|
||||
return imageView
|
||||
@ -80,30 +82,32 @@ class MessageTableViewCell: BaseTableViewCell {
|
||||
|
||||
layoutView()
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
func layoutView(){
|
||||
bodyStackView.snp.makeConstraints { (make) in
|
||||
func layoutView() {
|
||||
bodyStackView.snp.makeConstraints { make in
|
||||
make.left.top.equalToSuperview().offset(16)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
}
|
||||
titleLabel.snp.remakeConstraints { (make) in
|
||||
titleLabel.snp.remakeConstraints { make in
|
||||
make.left.equalTo(12)
|
||||
make.right.equalTo(-12)
|
||||
}
|
||||
bodyLabel.snp.remakeConstraints { (make) in
|
||||
bodyLabel.snp.remakeConstraints { make in
|
||||
make.left.right.equalTo(titleLabel)
|
||||
}
|
||||
urlLabel.snp.makeConstraints { (make) in
|
||||
urlLabel.snp.makeConstraints { make in
|
||||
make.left.right.equalTo(bodyLabel)
|
||||
}
|
||||
dateLabel.snp.remakeConstraints { (make) in
|
||||
dateLabel.snp.remakeConstraints { make in
|
||||
make.left.equalTo(bodyLabel)
|
||||
make.top.equalTo(bodyStackView.snp.bottom).offset(12)
|
||||
}
|
||||
separatorLine.snp.remakeConstraints { (make) in
|
||||
separatorLine.snp.remakeConstraints { make in
|
||||
make.left.right.bottom.equalToSuperview()
|
||||
make.top.equalTo(dateLabel.snp.bottom).offset(12)
|
||||
make.height.equalTo(10)
|
||||
@ -127,12 +131,11 @@ class MessageTableViewCell: BaseTableViewCell {
|
||||
viewModel.url.bind(to: self.urlLabel.rx.text).disposed(by: rx.reuseBag)
|
||||
viewModel.date.bind(to: self.dateLabel.rx.text).disposed(by: rx.reuseBag)
|
||||
|
||||
viewModel.title.map{ $0.count <= 0}.bind(to: self.titleLabel.rx.isHidden).disposed(by: rx.reuseBag)
|
||||
viewModel.url.map{ $0.count <= 0}.bind(to: self.urlLabel.rx.isHidden).disposed(by: rx.reuseBag)
|
||||
viewModel.title.map { $0.count <= 0 }.bind(to: self.titleLabel.rx.isHidden).disposed(by: rx.reuseBag)
|
||||
viewModel.url.map { $0.count <= 0 }.bind(to: self.urlLabel.rx.isHidden).disposed(by: rx.reuseBag)
|
||||
|
||||
self.urlLabel.gestureRecognizers?.first?.rx.event
|
||||
.map{[weak self] _ in self?.urlLabel.text ?? "" }
|
||||
.map { [weak self] _ in self?.urlLabel.text ?? "" }
|
||||
.bind(to: viewModel.urlTap).disposed(by: rx.reuseBag)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,12 +6,11 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Differentiator
|
||||
import Foundation
|
||||
import RxCocoa
|
||||
import Differentiator
|
||||
import RxDataSources
|
||||
|
||||
|
||||
class MessageTableViewCellViewModel: ViewModel {
|
||||
let message: Message
|
||||
|
||||
@ -22,7 +21,7 @@ class MessageTableViewCellViewModel: ViewModel {
|
||||
|
||||
let urlTap: PublishRelay<String>
|
||||
|
||||
init(message:Message) {
|
||||
init(message: Message) {
|
||||
self.message = message
|
||||
|
||||
self.title = BehaviorRelay<String>(value: message.title ?? "")
|
||||
@ -35,13 +34,12 @@ class MessageTableViewCellViewModel: ViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
struct MessageSection {
|
||||
var header: String
|
||||
var messages:[MessageTableViewCellViewModel]
|
||||
var messages: [MessageTableViewCellViewModel]
|
||||
}
|
||||
|
||||
extension MessageSection:AnimatableSectionModelType {
|
||||
extension MessageSection: AnimatableSectionModelType {
|
||||
typealias Item = MessageTableViewCellViewModel
|
||||
typealias Identity = String
|
||||
|
||||
@ -62,7 +60,7 @@ extension MessageSection:AnimatableSectionModelType {
|
||||
extension MessageTableViewCellViewModel: IdentifiableType {
|
||||
typealias Identity = String
|
||||
|
||||
var identity: String{
|
||||
var identity: String {
|
||||
return "\(self.message.id)"
|
||||
}
|
||||
|
||||
|
||||
@ -6,11 +6,10 @@
|
||||
// Copyright © 2018 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import UIKit
|
||||
|
||||
class PreviewCardCell: BaseTableViewCell {
|
||||
|
||||
let previewButton = IconButton(image: Icon.cm.skipForward, tintColor: Color.grey.base)
|
||||
let copyButton = IconButton(image: UIImage(named: "baseline_file_copy_white_24pt"), tintColor: Color.grey.base)
|
||||
|
||||
@ -21,6 +20,7 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
label.numberOfLines = 0
|
||||
return label
|
||||
}()
|
||||
|
||||
let bodyLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.font = RobotoFont.regular(with: 14)
|
||||
@ -37,12 +37,14 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
label.isUserInteractionEnabled = true
|
||||
return label
|
||||
}()
|
||||
let contentImageView:UIImageView = {
|
||||
|
||||
let contentImageView: UIImageView = {
|
||||
let imageView = UIImageView()
|
||||
imageView.contentMode = .scaleAspectFit
|
||||
return imageView
|
||||
}()
|
||||
let card:UIView = {
|
||||
|
||||
let card: UIView = {
|
||||
let view = UIView()
|
||||
view.backgroundColor = Color.white
|
||||
view.layer.cornerRadius = 2
|
||||
@ -60,6 +62,7 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
return label
|
||||
}()
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
@ -73,23 +76,22 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
card.addSubview(copyButton)
|
||||
card.addSubview(previewButton)
|
||||
|
||||
card.snp.makeConstraints { (make) in
|
||||
card.snp.makeConstraints { make in
|
||||
make.left.top.equalToSuperview().offset(16)
|
||||
make.right.equalToSuperview().offset(-16)
|
||||
make.bottom.equalToSuperview()
|
||||
}
|
||||
previewButton.snp.makeConstraints { (make) in
|
||||
previewButton.snp.makeConstraints { make in
|
||||
make.right.equalToSuperview().offset(-10)
|
||||
make.centerY.equalTo(card.snp.top).offset(40)
|
||||
make.width.height.equalTo(40)
|
||||
}
|
||||
copyButton.snp.makeConstraints { (make) in
|
||||
copyButton.snp.makeConstraints { make in
|
||||
make.right.equalTo(previewButton.snp.left).offset(-10)
|
||||
make.centerY.equalTo(previewButton)
|
||||
make.width.height.equalTo(40)
|
||||
}
|
||||
|
||||
|
||||
let titleStackView = UIStackView()
|
||||
titleStackView.axis = .vertical
|
||||
titleStackView.addArrangedSubview(titleLabel)
|
||||
@ -97,20 +99,19 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
|
||||
card.addSubview(titleStackView)
|
||||
|
||||
titleLabel.snp.makeConstraints { (make) in
|
||||
titleLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(15)
|
||||
}
|
||||
bodyLabel.snp.makeConstraints { (make) in
|
||||
bodyLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(15)
|
||||
}
|
||||
|
||||
titleStackView.snp.makeConstraints { (make) in
|
||||
titleStackView.snp.makeConstraints { make in
|
||||
make.centerY.equalTo(copyButton)
|
||||
make.left.equalToSuperview()
|
||||
make.right.equalTo(copyButton.snp.left)
|
||||
}
|
||||
|
||||
|
||||
let contentStackView = UIStackView()
|
||||
contentStackView.axis = .vertical
|
||||
contentStackView.spacing = 20
|
||||
@ -121,18 +122,18 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
contentStackView.addArrangedSubview(contentLabel)
|
||||
contentStackView.addArrangedSubview(noticeLabel)
|
||||
|
||||
contentLabel.snp.makeConstraints { (make) in
|
||||
contentLabel.snp.makeConstraints { make in
|
||||
make.left.equalToSuperview().offset(12)
|
||||
make.right.equalToSuperview().offset(-12)
|
||||
}
|
||||
contentImageView.snp.remakeConstraints { (make) in
|
||||
contentImageView.snp.remakeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
}
|
||||
noticeLabel.snp.makeConstraints { (make) in
|
||||
noticeLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(10)
|
||||
make.right.equalTo(-10)
|
||||
}
|
||||
contentStackView.snp.makeConstraints { (make) in
|
||||
contentStackView.snp.makeConstraints { make in
|
||||
make.left.right.equalToSuperview()
|
||||
make.top.equalTo(previewButton.snp.bottom).offset(20)
|
||||
make.bottom.equalToSuperview().offset(-10)
|
||||
@ -141,7 +142,6 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
noticeLabel.addGestureRecognizer(UITapGestureRecognizer())
|
||||
}
|
||||
|
||||
|
||||
override func bindViewModel(model: ViewModel) {
|
||||
guard let viewModel = model as? PreviewCardCellViewModel else {
|
||||
return
|
||||
@ -155,7 +155,7 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
viewModel.notice
|
||||
.bind(to: self.noticeLabel.rx.attributedText).disposed(by: rx.reuseBag)
|
||||
viewModel.contentImage
|
||||
.compactMap{ $0 }
|
||||
.compactMap { $0 }
|
||||
.bind(to: self.contentImageView.rx.image)
|
||||
.disposed(by: rx.reuseBag)
|
||||
viewModel.contentImage
|
||||
@ -163,34 +163,33 @@ class PreviewCardCell: BaseTableViewCell {
|
||||
.bind(to: self.contentImageView.rx.isHidden)
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
//点击通知
|
||||
// 点击通知
|
||||
noticeLabel.gestureRecognizers!.first!
|
||||
.rx.event
|
||||
.compactMap{[weak weakModel = viewModel](_) -> ViewModel? in
|
||||
//仅在有 moreViewModel 时 点击
|
||||
return weakModel?.previewModel.moreViewModel
|
||||
.compactMap { [weak weakModel = viewModel] _ -> ViewModel? in
|
||||
// 仅在有 moreViewModel 时 点击
|
||||
weakModel?.previewModel.moreViewModel
|
||||
}
|
||||
.bind(to: viewModel.noticeTap)
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
//点击复制
|
||||
copyButton.rx.tap.map {[weak self] () -> String in
|
||||
return self?.contentLabel.text ?? ""
|
||||
// 点击复制
|
||||
copyButton.rx.tap.map { [weak self] () -> String in
|
||||
self?.contentLabel.text ?? ""
|
||||
}
|
||||
.bind(to: viewModel.copy)
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
//点击预览
|
||||
previewButton.rx.tap.compactMap {[weak self] () -> URL? in
|
||||
// 点击预览
|
||||
previewButton.rx.tap.compactMap { [weak self] () -> URL? in
|
||||
if let urlStr = self?.contentLabel.text?.urlEncoded(),
|
||||
let url = URL(string: urlStr){
|
||||
let url = URL(string: urlStr)
|
||||
{
|
||||
return url
|
||||
}
|
||||
return nil
|
||||
}
|
||||
.bind(to: viewModel.preview)
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -20,8 +20,8 @@ class PreviewCardCellViewModel: ViewModel {
|
||||
let copy = PublishRelay<String>()
|
||||
let preview = PublishRelay<URL>()
|
||||
|
||||
let previewModel:PreviewModel
|
||||
init( previewModel:PreviewModel, clientState: Driver<Client.ClienState> ) {
|
||||
let previewModel: PreviewModel
|
||||
init(previewModel: PreviewModel, clientState: Driver<Client.ClienState>) {
|
||||
self.previewModel = previewModel
|
||||
contentImage = BehaviorRelay<UIImage?>(value: previewModel.image)
|
||||
|
||||
@ -39,30 +39,29 @@ class PreviewCardCellViewModel: ViewModel {
|
||||
// 因为这时可能 ServerManager.shared.currentAddress 或 Client.shared.key 发生了改变。
|
||||
// 这不是一个好的写法,viewModel 应尽可能只依赖固定的 input ,而不应依赖不可预测的外部变量( currentAddress 与 key )。
|
||||
// 但这个项目是由 MVC 临时重构为 MVVM ,之前是这样写的,所以懒得改动了。
|
||||
clientState.compactMap({[weak self] (_) -> NSAttributedString? in
|
||||
return self?.contentAttrStr()
|
||||
})
|
||||
clientState.compactMap { [weak self] _ -> NSAttributedString? in
|
||||
self?.contentAttrStr()
|
||||
}
|
||||
.drive(content)
|
||||
.disposed(by: rx.disposeBag)
|
||||
|
||||
|
||||
let noticeStr = "\(previewModel.notice ?? "")"
|
||||
let noticeAttrStr = NSMutableAttributedString(string: noticeStr, attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.grey.base,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: 12)
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: 12)
|
||||
])
|
||||
|
||||
if let moreInfo = previewModel.moreInfo {
|
||||
noticeAttrStr.append(NSMutableAttributedString(string: " \(moreInfo)", attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.blue.base,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: 12)
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: 12)
|
||||
]))
|
||||
}
|
||||
notice.accept(noticeAttrStr)
|
||||
}
|
||||
|
||||
func contentAttrStr() -> NSAttributedString {
|
||||
var fontSize:CGFloat = 14
|
||||
var fontSize: CGFloat = 14
|
||||
if UIScreen.main.bounds.size.width <= 320 {
|
||||
fontSize = 11
|
||||
}
|
||||
@ -70,31 +69,31 @@ class PreviewCardCellViewModel: ViewModel {
|
||||
let attrStr = NSMutableAttributedString(string: "")
|
||||
attrStr.append(NSAttributedString(string: serverUrl.absoluteString, attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.grey.darken4,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
|
||||
attrStr.append(NSAttributedString(string: "/\(Client.shared.key ?? "Your Key")", attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.grey.darken3,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
|
||||
if let modelTitle = previewModel.title {
|
||||
attrStr.append(NSAttributedString(string: "/\(modelTitle)", attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.grey.darken1,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
}
|
||||
if let modelBody = previewModel.body {
|
||||
attrStr.append(NSAttributedString(string: "/\(modelBody)", attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.grey.base,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
}
|
||||
if let queryParameter = previewModel.queryParameter {
|
||||
attrStr.append(NSAttributedString(string: "?\(queryParameter)", attributes: [
|
||||
NSAttributedString.Key.foregroundColor: Color.grey.lighten1,
|
||||
NSAttributedString.Key.font : RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
NSAttributedString.Key.font: RobotoFont.regular(with: fontSize)
|
||||
]))
|
||||
}
|
||||
|
||||
return attrStr
|
||||
|
||||
@ -6,24 +6,26 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import Material
|
||||
import AVKit
|
||||
import Material
|
||||
import UIKit
|
||||
|
||||
class SoundCell: BaseTableViewCell {
|
||||
let copyButton = IconButton(image: UIImage(named: "baseline_file_copy_white_24pt"), tintColor: Color.grey.base)
|
||||
let nameLabel:UILabel = {
|
||||
let nameLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.fontSize = 14
|
||||
label.textColor = Color.darkText.primary
|
||||
return label
|
||||
}()
|
||||
let durationLabel:UILabel = {
|
||||
|
||||
let durationLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.fontSize = 12
|
||||
label.textColor = Color.darkText.secondary
|
||||
return label
|
||||
}()
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
self.selectionStyle = .none
|
||||
@ -32,25 +34,27 @@ class SoundCell: BaseTableViewCell {
|
||||
self.contentView.addSubview(durationLabel)
|
||||
self.contentView.addSubview(copyButton)
|
||||
|
||||
nameLabel.snp.makeConstraints { (make) in
|
||||
nameLabel.snp.makeConstraints { make in
|
||||
make.left.top.equalToSuperview().offset(15)
|
||||
}
|
||||
durationLabel.snp.makeConstraints { (make) in
|
||||
durationLabel.snp.makeConstraints { make in
|
||||
make.left.equalTo(nameLabel)
|
||||
make.top.equalTo(nameLabel.snp.bottom).offset(5)
|
||||
make.bottom.equalToSuperview().offset(-15)
|
||||
}
|
||||
copyButton.snp.makeConstraints { (make) in
|
||||
copyButton.snp.makeConstraints { make in
|
||||
make.right.equalToSuperview().offset(-15)
|
||||
make.centerY.equalToSuperview()
|
||||
make.width.height.equalTo(40)
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
override func bindViewModel(model:ViewModel){
|
||||
override func bindViewModel(model: ViewModel) {
|
||||
super.bindViewModel(model: model)
|
||||
guard let viewModel = model as? SoundCellViewModel else {
|
||||
return
|
||||
@ -60,12 +64,12 @@ class SoundCell: BaseTableViewCell {
|
||||
.bind(to: nameLabel.rx.text)
|
||||
.disposed(by: rx.reuseBag)
|
||||
viewModel.duration
|
||||
.map { String(format: "%.2g second(s)", CMTimeGetSeconds($0) ) }
|
||||
.map { String(format: "%.2g second(s)", CMTimeGetSeconds($0)) }
|
||||
.bind(to: durationLabel.rx.text)
|
||||
.disposed(by: rx.reuseBag)
|
||||
|
||||
copyButton.rx.tap
|
||||
.map{ viewModel.name.value }
|
||||
.map { viewModel.name.value }
|
||||
.bind(to: viewModel.copyNameAction)
|
||||
.disposed(by: rx.reuseBag)
|
||||
}
|
||||
|
||||
@ -6,18 +6,18 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import RxSwift
|
||||
import RxCocoa
|
||||
import AVKit
|
||||
import Foundation
|
||||
import RxCocoa
|
||||
import RxSwift
|
||||
|
||||
class SoundCellViewModel:ViewModel {
|
||||
class SoundCellViewModel: ViewModel {
|
||||
let name = BehaviorRelay<String>(value: "")
|
||||
let duration = BehaviorRelay<CMTime>(value: .zero)
|
||||
|
||||
|
||||
let copyNameAction = PublishRelay<String>()
|
||||
let playAction = PublishRelay<CFURL>()
|
||||
|
||||
|
||||
let model: AVURLAsset
|
||||
init(model: AVURLAsset) {
|
||||
self.model = model
|
||||
|
||||
@ -9,18 +9,21 @@
|
||||
import UIKit
|
||||
|
||||
class SpacerCell: UITableViewCell {
|
||||
var height:CGFloat = 0 {
|
||||
didSet{
|
||||
var height: CGFloat = 0 {
|
||||
didSet {
|
||||
self.contentView.snp.remakeConstraints { make in
|
||||
make.height.equalTo(height)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: style, reuseIdentifier: reuseIdentifier)
|
||||
self.backgroundColor = UIColor.clear
|
||||
self.selectionStyle = .none
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@ -9,14 +9,14 @@
|
||||
import UIKit
|
||||
|
||||
class DetailTextCell: UITableViewCell {
|
||||
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
|
||||
self.selectionStyle = .none
|
||||
self.accessoryType = .disclosureIndicator
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@ -8,40 +8,40 @@
|
||||
|
||||
import UIKit
|
||||
|
||||
//如果第一个 item 是系统自带的 UIBarButtonItem ,则距离导航栏左右距离只有8
|
||||
//自己定义的的则最少有16,太宽了
|
||||
//所以先用 一个 fixedSpace UIBarButtonItem 先把距离给缩短点,
|
||||
//然后用个 AlignmentRectInsetsOverridable 把自己的按钮往 左/右 挪动,减少距离
|
||||
//用 HitTestSlopable 增加点击区域
|
||||
// 如果第一个 item 是系统自带的 UIBarButtonItem ,则距离导航栏左右距离只有8
|
||||
// 自己定义的的则最少有16,太宽了
|
||||
// 所以先用 一个 fixedSpace UIBarButtonItem 先把距离给缩短点,
|
||||
// 然后用个 AlignmentRectInsetsOverridable 把自己的按钮往 左/右 挪动,减少距离
|
||||
// 用 HitTestSlopable 增加点击区域
|
||||
extension UINavigationItem {
|
||||
|
||||
func setLeftBarButtonItem(item: UIBarButtonItem){
|
||||
func setLeftBarButtonItem(item: UIBarButtonItem) {
|
||||
setBarButtonItems(items: [item], left: true)
|
||||
}
|
||||
func setRightBarButtonItem(item: UIBarButtonItem){
|
||||
|
||||
func setRightBarButtonItem(item: UIBarButtonItem) {
|
||||
setBarButtonItems(items: [item], left: false)
|
||||
}
|
||||
|
||||
func setBarButtonItems(items: [UIBarButtonItem], left:Bool){
|
||||
|
||||
func setBarButtonItems(items: [UIBarButtonItem], left: Bool) {
|
||||
guard items.count > 0 else {
|
||||
self.leftBarButtonItems = nil
|
||||
return
|
||||
}
|
||||
var buttonItems = items
|
||||
if #available(iOS 11.0, *) {
|
||||
buttonItems.forEach { (item) in
|
||||
guard let view = item.customView else {return}
|
||||
buttonItems.forEach { item in
|
||||
guard let view = item.customView else { return }
|
||||
item.customView?.translatesAutoresizingMaskIntoConstraints = false
|
||||
(item.customView as? HitTestSlopable)?.hitTestSlop = UIEdgeInsets(top: -10, left: -10, bottom: -10, right: -10)
|
||||
(item.customView as? AlignmentRectInsetsOverridable)?.alignmentRectInsetsOverride = UIEdgeInsets(top: 0, left: left ? 8 : -8, bottom: 0, right: left ? -8 : 8)
|
||||
item.customView?.snp.makeConstraints({ (make) in
|
||||
item.customView?.snp.makeConstraints { make in
|
||||
make.width.equalTo(view.bounds.size.width > 24 ? view.bounds.width : 24)
|
||||
make.height.equalTo(view.bounds.size.height > 24 ? view.bounds.height : 24)
|
||||
})
|
||||
}
|
||||
}
|
||||
buttonItems.insert(UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil), at: 0)
|
||||
}
|
||||
else{
|
||||
else {
|
||||
let spacer = UIBarButtonItem(barButtonSystemItem: .fixedSpace, target: nil, action: nil)
|
||||
spacer.width = -8
|
||||
buttonItems.insert(spacer, at: 0)
|
||||
|
||||
@ -6,22 +6,22 @@
|
||||
// Copyright © 2020 Fin. All rights reserved.
|
||||
//
|
||||
|
||||
import UIKit
|
||||
import CloudKit
|
||||
import UIKit
|
||||
|
||||
class iCloudStatusCell: UITableViewCell {
|
||||
override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
|
||||
super.init(style: .value1, reuseIdentifier: reuseIdentifier)
|
||||
self.selectionStyle = .none
|
||||
|
||||
|
||||
self.textLabel?.text = NSLocalizedString("iCloudSatatus")
|
||||
self.detailTextLabel?.text = ""
|
||||
CKContainer.default().accountStatus { (status, error) in
|
||||
CKContainer.default().accountStatus { status, _ in
|
||||
dispatch_sync_safely_main_queue {
|
||||
switch status {
|
||||
case .available:
|
||||
self.detailTextLabel?.text = NSLocalizedString("available")
|
||||
|
||||
|
||||
case .noAccount, .restricted:
|
||||
self.detailTextLabel?.text = NSLocalizedString("restricted")
|
||||
case .couldNotDetermine:
|
||||
@ -32,6 +32,8 @@ class iCloudStatusCell: UITableViewCell {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(*, unavailable)
|
||||
required init?(coder aDecoder: NSCoder) {
|
||||
fatalError("init(coder:) has not been implemented")
|
||||
}
|
||||
|
||||
@ -11,59 +11,58 @@ import UserNotifications
|
||||
import UserNotificationsUI
|
||||
|
||||
class NotificationViewController: UIViewController, UNNotificationContentExtension {
|
||||
|
||||
let noticeLabel:UILabel = {
|
||||
let noticeLabel: UILabel = {
|
||||
let label = UILabel()
|
||||
label.textColor = UIColor.init(named: "notification_copy_color")
|
||||
label.textColor = UIColor(named: "notification_copy_color")
|
||||
label.text = NSLocalizedString("Copy", comment: "")
|
||||
label.font = UIFont.systemFont(ofSize: 16)
|
||||
label.textAlignment = .center
|
||||
return label
|
||||
}()
|
||||
|
||||
|
||||
override func viewDidLoad() {
|
||||
super.viewDidLoad()
|
||||
self.view.addSubview(self.noticeLabel)
|
||||
self.preferredContentSize = CGSize(width: 0, height: 1)
|
||||
}
|
||||
|
||||
override func viewWillAppear(_ animated: Bool) {
|
||||
super.viewWillAppear(animated)
|
||||
self.preferredContentSize = CGSize(width: 0, height: 1)
|
||||
}
|
||||
|
||||
override func viewDidAppear(_ animated: Bool) {
|
||||
super.viewDidAppear(animated)
|
||||
self.preferredContentSize = CGSize(width: 0, height: 1)
|
||||
}
|
||||
|
||||
|
||||
func didReceive(_ notification: UNNotification) {
|
||||
guard notification.request.content.userInfo["autocopy"] as? String == "1"
|
||||
|| notification.request.content.userInfo["automaticallycopy"] as? String == "1" else {
|
||||
|| notification.request.content.userInfo["automaticallycopy"] as? String == "1"
|
||||
else {
|
||||
return
|
||||
}
|
||||
if let copy = notification.request.content.userInfo["copy"] as? String
|
||||
{
|
||||
if let copy = notification.request.content.userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
}
|
||||
else{
|
||||
else {
|
||||
UIPasteboard.general.string = notification.request.content.body
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
func didReceive(_ response: UNNotificationResponse, completionHandler completion: @escaping (UNNotificationContentExtensionResponseOption) -> Void) {
|
||||
|
||||
let userInfo = response.notification.request.content.userInfo
|
||||
|
||||
|
||||
if let copy = userInfo["copy"] as? String {
|
||||
UIPasteboard.general.string = copy
|
||||
}
|
||||
else{
|
||||
else {
|
||||
UIPasteboard.general.string = response.notification.request.content.body
|
||||
}
|
||||
|
||||
self.preferredContentSize = CGSize(width: 0, height: 40)
|
||||
self.noticeLabel.frame = CGRect(x: 0, y: 0, width: self.view.bounds.width, height: 40)
|
||||
|
||||
completion(.doNotDismiss)
|
||||
|
||||
completion(.doNotDismiss)
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user