├── Forum.xcodeproj ├── project.pbxproj ├── project.xcworkspace │ ├── contents.xcworkspacedata │ └── xcshareddata │ │ └── IDEWorkspaceChecks.plist └── xcuserdata │ └── oscar.xcuserdatad │ └── xcschemes │ └── xcschememanagement.plist ├── Forum.xcworkspace ├── contents.xcworkspacedata └── xcshareddata │ └── IDEWorkspaceChecks.plist ├── Forum ├── AppDelegate.swift ├── Assets.xcassets │ ├── AccentColor.colorset │ │ └── Contents.json │ ├── AppIcon.appiconset │ │ ├── 100.png │ │ ├── 1024.png │ │ ├── 114.png │ │ ├── 120.png │ │ ├── 128.png │ │ ├── 144.png │ │ ├── 152.png │ │ ├── 16.png │ │ ├── 167.png │ │ ├── 172.png │ │ ├── 180.png │ │ ├── 196.png │ │ ├── 20.png │ │ ├── 216.png │ │ ├── 256.png │ │ ├── 29.png │ │ ├── 32.png │ │ ├── 40.png │ │ ├── 48.png │ │ ├── 50.png │ │ ├── 512.png │ │ ├── 55.png │ │ ├── 57.png │ │ ├── 58.png │ │ ├── 60.png │ │ ├── 64.png │ │ ├── 72.png │ │ ├── 76.png │ │ ├── 80.png │ │ ├── 87.png │ │ ├── 88.png │ │ └── Contents.json │ ├── Contents.json │ ├── avator.imageset │ │ ├── 1E676D35-3D9A-4FD0-8BEA-7077A7829F9B.jpeg │ │ └── Contents.json │ ├── hat20.imageset │ │ ├── Contents.json │ │ └── hat20.png │ ├── hat40.imageset │ │ ├── Contents.json │ │ └── hat40.png │ ├── hat60.imageset │ │ ├── Contents.json │ │ └── hat60.png │ ├── hatw100.imageset │ │ ├── Contents.json │ │ └── hatw100.png │ ├── hatw50.imageset │ │ ├── Contents.json │ │ └── hatw50.png │ ├── hatw80.imageset │ │ ├── Contents.json │ │ └── hatw80.png │ └── icon4.imageset │ │ ├── Contents.json │ │ └── icon4.png ├── Base.lproj │ ├── LaunchScreen.storyboard │ └── Main.storyboard ├── Forum.entitlements ├── Forum.xcdatamodeld │ ├── .xccurrentversion │ └── Forum.xcdatamodel │ │ └── contents ├── Info.plist ├── Misc │ ├── DataTypes.swift │ ├── Globals.swift │ ├── Network.swift │ └── Utilities.swift ├── OtherViews │ ├── AboutVC.swift │ ├── LoginVC.swift │ ├── MiscTableViewCell.swift │ ├── MiscVC.swift │ └── NewThreadVC.swift ├── SceneDelegate.swift └── TableViews │ ├── MainCell.swift │ ├── MainCell.xib │ ├── MainVC.swift │ ├── MainVCMenu.swift │ ├── MainVCRefresh.swift │ ├── MainVCScroll.swift │ ├── MainVCSearch.swift │ ├── MainVCText.swift │ ├── SettingCell.swift │ └── TabBarController.swift ├── ForumShare └── ForumShare.entitlements ├── ForumTests ├── ForumTests.swift └── Info.plist ├── ForumUITests ├── ForumUITests.swift └── Info.plist ├── LICENSE ├── Podfile └── README.md /Forum.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /Forum.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Forum.xcodeproj/xcuserdata/oscar.xcuserdatad/xcschemes/xcschememanagement.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SchemeUserState 6 | 7 | Forum.xcscheme_^#shared#^_ 8 | 9 | orderHint 10 | 0 11 | 12 | ForumShare.xcscheme_^#shared#^_ 13 | 14 | orderHint 15 | 13 16 | 17 | 18 | SuppressBuildableAutocreation 19 | 20 | 55AA46562516739A00AAF708 21 | 22 | primary 23 | 24 | 25 | 55AA466F2516739B00AAF708 26 | 27 | primary 28 | 29 | 30 | 55AA467A2516739B00AAF708 31 | 32 | primary 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /Forum.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Forum.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /Forum/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AppDelegate.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/9/20. 6 | // 7 | 8 | import UIKit 9 | import CoreData 10 | 11 | @main 12 | class AppDelegate: UIResponder, UIApplicationDelegate { 13 | 14 | var window: UIWindow? 15 | 16 | func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { 17 | // Override point for customization after application launch. 18 | return true 19 | } 20 | 21 | // MARK: UISceneSession Lifecycle 22 | 23 | @available(iOS 13.0, *) 24 | func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration { 25 | // Called when a new scene session is being created. 26 | // Use this method to select a configuration to create the new scene with. 27 | return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role) 28 | } 29 | 30 | @available(iOS 13.0, *) 31 | func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set) { 32 | // Called when the user discards a scene session. 33 | // If any sessions were discarded while the application was not running, this will be called shortly after application:didFinishLaunchingWithOptions. 34 | // Use this method to release any resources that were specific to the discarded scenes, as they will not return. 35 | } 36 | 37 | // MARK: - Core Data stack 38 | 39 | lazy var persistentContainer: NSPersistentContainer = { 40 | /* 41 | The persistent container for the application. This implementation 42 | creates and returns a container, having loaded the store for the 43 | application to it. This property is optional since there are legitimate 44 | error conditions that could cause the creation of the store to fail. 45 | */ 46 | let container = NSPersistentContainer(name: "Forum") 47 | container.loadPersistentStores(completionHandler: { (storeDescription, error) in 48 | if let error = error as NSError? { 49 | // Replace this implementation with code to handle the error appropriately. 50 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 51 | 52 | /* 53 | Typical reasons for an error here include: 54 | * The parent directory does not exist, cannot be created, or disallows writing. 55 | * The persistent store is not accessible, due to permissions or data protection when the device is locked. 56 | * The device is out of space. 57 | * The store could not be migrated to the current model version. 58 | Check the error message to determine what the actual problem was. 59 | */ 60 | fatalError("Unresolved error \(error), \(error.userInfo)") 61 | } 62 | }) 63 | return container 64 | }() 65 | 66 | // MARK: - Core Data Saving support 67 | 68 | func saveContext () { 69 | let context = persistentContainer.viewContext 70 | if context.hasChanges { 71 | do { 72 | try context.save() 73 | } catch { 74 | // Replace this implementation with code to handle the error appropriately. 75 | // fatalError() causes the application to generate a crash log and terminate. You should not use this function in a shipping application, although it may be useful during development. 76 | let nserror = error as NSError 77 | fatalError("Unresolved error \(nserror), \(nserror.userInfo)") 78 | } 79 | } 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AccentColor.colorset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "colors" : [ 3 | { 4 | "color" : { 5 | "platform" : "ios", 6 | "reference" : "systemBlueColor" 7 | }, 8 | "idiom" : "universal" 9 | } 10 | ], 11 | "info" : { 12 | "author" : "xcode", 13 | "version" : 1 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/100.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/1024.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/114.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/114.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/120.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/128.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/144.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/152.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/16.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/167.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/172.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/172.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/180.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/196.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/196.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/20.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/216.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/216.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/256.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/29.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/29.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/32.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/40.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/48.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/50.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/512.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/55.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/55.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/57.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/57.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/58.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/58.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/60.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/64.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/72.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/76.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/76.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/80.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/87.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/87.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/88.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/AppIcon.appiconset/88.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "40.png", 5 | "idiom" : "iphone", 6 | "scale" : "2x", 7 | "size" : "20x20" 8 | }, 9 | { 10 | "filename" : "60.png", 11 | "idiom" : "iphone", 12 | "scale" : "3x", 13 | "size" : "20x20" 14 | }, 15 | { 16 | "filename" : "29.png", 17 | "idiom" : "iphone", 18 | "scale" : "1x", 19 | "size" : "29x29" 20 | }, 21 | { 22 | "filename" : "58.png", 23 | "idiom" : "iphone", 24 | "scale" : "2x", 25 | "size" : "29x29" 26 | }, 27 | { 28 | "filename" : "87.png", 29 | "idiom" : "iphone", 30 | "scale" : "3x", 31 | "size" : "29x29" 32 | }, 33 | { 34 | "filename" : "80.png", 35 | "idiom" : "iphone", 36 | "scale" : "2x", 37 | "size" : "40x40" 38 | }, 39 | { 40 | "filename" : "120.png", 41 | "idiom" : "iphone", 42 | "scale" : "3x", 43 | "size" : "40x40" 44 | }, 45 | { 46 | "filename" : "57.png", 47 | "idiom" : "iphone", 48 | "scale" : "1x", 49 | "size" : "57x57" 50 | }, 51 | { 52 | "filename" : "114.png", 53 | "idiom" : "iphone", 54 | "scale" : "2x", 55 | "size" : "57x57" 56 | }, 57 | { 58 | "filename" : "120.png", 59 | "idiom" : "iphone", 60 | "scale" : "2x", 61 | "size" : "60x60" 62 | }, 63 | { 64 | "filename" : "180.png", 65 | "idiom" : "iphone", 66 | "scale" : "3x", 67 | "size" : "60x60" 68 | }, 69 | { 70 | "filename" : "20.png", 71 | "idiom" : "ipad", 72 | "scale" : "1x", 73 | "size" : "20x20" 74 | }, 75 | { 76 | "filename" : "40.png", 77 | "idiom" : "ipad", 78 | "scale" : "2x", 79 | "size" : "20x20" 80 | }, 81 | { 82 | "filename" : "29.png", 83 | "idiom" : "ipad", 84 | "scale" : "1x", 85 | "size" : "29x29" 86 | }, 87 | { 88 | "filename" : "58.png", 89 | "idiom" : "ipad", 90 | "scale" : "2x", 91 | "size" : "29x29" 92 | }, 93 | { 94 | "filename" : "40.png", 95 | "idiom" : "ipad", 96 | "scale" : "1x", 97 | "size" : "40x40" 98 | }, 99 | { 100 | "filename" : "80.png", 101 | "idiom" : "ipad", 102 | "scale" : "2x", 103 | "size" : "40x40" 104 | }, 105 | { 106 | "filename" : "50.png", 107 | "idiom" : "ipad", 108 | "scale" : "1x", 109 | "size" : "50x50" 110 | }, 111 | { 112 | "filename" : "100.png", 113 | "idiom" : "ipad", 114 | "scale" : "2x", 115 | "size" : "50x50" 116 | }, 117 | { 118 | "filename" : "72.png", 119 | "idiom" : "ipad", 120 | "scale" : "1x", 121 | "size" : "72x72" 122 | }, 123 | { 124 | "filename" : "144.png", 125 | "idiom" : "ipad", 126 | "scale" : "2x", 127 | "size" : "72x72" 128 | }, 129 | { 130 | "filename" : "76.png", 131 | "idiom" : "ipad", 132 | "scale" : "1x", 133 | "size" : "76x76" 134 | }, 135 | { 136 | "filename" : "152.png", 137 | "idiom" : "ipad", 138 | "scale" : "2x", 139 | "size" : "76x76" 140 | }, 141 | { 142 | "filename" : "167.png", 143 | "idiom" : "ipad", 144 | "scale" : "2x", 145 | "size" : "83.5x83.5" 146 | }, 147 | { 148 | "filename" : "1024.png", 149 | "idiom" : "ios-marketing", 150 | "scale" : "1x", 151 | "size" : "1024x1024" 152 | }, 153 | { 154 | "filename" : "48.png", 155 | "idiom" : "watch", 156 | "role" : "notificationCenter", 157 | "scale" : "2x", 158 | "size" : "24x24", 159 | "subtype" : "38mm" 160 | }, 161 | { 162 | "filename" : "55.png", 163 | "idiom" : "watch", 164 | "role" : "notificationCenter", 165 | "scale" : "2x", 166 | "size" : "27.5x27.5", 167 | "subtype" : "42mm" 168 | }, 169 | { 170 | "filename" : "58.png", 171 | "idiom" : "watch", 172 | "role" : "companionSettings", 173 | "scale" : "2x", 174 | "size" : "29x29" 175 | }, 176 | { 177 | "filename" : "87.png", 178 | "idiom" : "watch", 179 | "role" : "companionSettings", 180 | "scale" : "3x", 181 | "size" : "29x29" 182 | }, 183 | { 184 | "filename" : "80.png", 185 | "idiom" : "watch", 186 | "role" : "appLauncher", 187 | "scale" : "2x", 188 | "size" : "40x40", 189 | "subtype" : "38mm" 190 | }, 191 | { 192 | "filename" : "88.png", 193 | "idiom" : "watch", 194 | "role" : "appLauncher", 195 | "scale" : "2x", 196 | "size" : "44x44", 197 | "subtype" : "40mm" 198 | }, 199 | { 200 | "filename" : "100.png", 201 | "idiom" : "watch", 202 | "role" : "appLauncher", 203 | "scale" : "2x", 204 | "size" : "50x50", 205 | "subtype" : "44mm" 206 | }, 207 | { 208 | "filename" : "172.png", 209 | "idiom" : "watch", 210 | "role" : "quickLook", 211 | "scale" : "2x", 212 | "size" : "86x86", 213 | "subtype" : "38mm" 214 | }, 215 | { 216 | "filename" : "196.png", 217 | "idiom" : "watch", 218 | "role" : "quickLook", 219 | "scale" : "2x", 220 | "size" : "98x98", 221 | "subtype" : "42mm" 222 | }, 223 | { 224 | "filename" : "216.png", 225 | "idiom" : "watch", 226 | "role" : "quickLook", 227 | "scale" : "2x", 228 | "size" : "108x108", 229 | "subtype" : "44mm" 230 | }, 231 | { 232 | "filename" : "1024.png", 233 | "idiom" : "watch-marketing", 234 | "scale" : "1x", 235 | "size" : "1024x1024" 236 | }, 237 | { 238 | "filename" : "16.png", 239 | "idiom" : "mac", 240 | "scale" : "1x", 241 | "size" : "16x16" 242 | }, 243 | { 244 | "filename" : "32.png", 245 | "idiom" : "mac", 246 | "scale" : "2x", 247 | "size" : "16x16" 248 | }, 249 | { 250 | "filename" : "32.png", 251 | "idiom" : "mac", 252 | "scale" : "1x", 253 | "size" : "32x32" 254 | }, 255 | { 256 | "filename" : "64.png", 257 | "idiom" : "mac", 258 | "scale" : "2x", 259 | "size" : "32x32" 260 | }, 261 | { 262 | "filename" : "128.png", 263 | "idiom" : "mac", 264 | "scale" : "1x", 265 | "size" : "128x128" 266 | }, 267 | { 268 | "filename" : "256.png", 269 | "idiom" : "mac", 270 | "scale" : "2x", 271 | "size" : "128x128" 272 | }, 273 | { 274 | "filename" : "256.png", 275 | "idiom" : "mac", 276 | "scale" : "1x", 277 | "size" : "256x256" 278 | }, 279 | { 280 | "filename" : "512.png", 281 | "idiom" : "mac", 282 | "scale" : "2x", 283 | "size" : "256x256" 284 | }, 285 | { 286 | "filename" : "512.png", 287 | "idiom" : "mac", 288 | "scale" : "1x", 289 | "size" : "512x512" 290 | }, 291 | { 292 | "filename" : "1024.png", 293 | "idiom" : "mac", 294 | "scale" : "2x", 295 | "size" : "512x512" 296 | } 297 | ], 298 | "info" : { 299 | "author" : "xcode", 300 | "version" : 1 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "info" : { 3 | "author" : "xcode", 4 | "version" : 1 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/avator.imageset/1E676D35-3D9A-4FD0-8BEA-7077A7829F9B.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/avator.imageset/1E676D35-3D9A-4FD0-8BEA-7077A7829F9B.jpeg -------------------------------------------------------------------------------- /Forum/Assets.xcassets/avator.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "1E676D35-3D9A-4FD0-8BEA-7077A7829F9B.jpeg", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hat20.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hat20.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hat20.imageset/hat20.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/hat20.imageset/hat20.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hat40.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hat40.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hat40.imageset/hat40.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/hat40.imageset/hat40.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hat60.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hat60.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hat60.imageset/hat60.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/hat60.imageset/hat60.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hatw100.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hatw100.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hatw100.imageset/hatw100.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/hatw100.imageset/hatw100.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hatw50.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hatw50.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hatw50.imageset/hatw50.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/hatw50.imageset/hatw50.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hatw80.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "hatw80.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/hatw80.imageset/hatw80.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/hatw80.imageset/hatw80.png -------------------------------------------------------------------------------- /Forum/Assets.xcassets/icon4.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "filename" : "icon4.png", 5 | "idiom" : "universal", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "scale" : "2x" 11 | }, 12 | { 13 | "idiom" : "universal", 14 | "scale" : "3x" 15 | } 16 | ], 17 | "info" : { 18 | "author" : "xcode", 19 | "version" : 1 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /Forum/Assets.xcassets/icon4.imageset/icon4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oscardhc/Forum/a753a34b8763196499341d6c5c9a56d0c69b1de4/Forum/Assets.xcassets/icon4.imageset/icon4.png -------------------------------------------------------------------------------- /Forum/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /Forum/Forum.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /Forum/Forum.xcdatamodeld/.xccurrentversion: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | _XCCurrentVersionName 6 | Forum.xcdatamodel 7 | 8 | 9 | -------------------------------------------------------------------------------- /Forum/Forum.xcdatamodeld/Forum.xcdatamodel/contents: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /Forum/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | zh_CN 7 | CFBundleDisplayName 8 | 无可奉告 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 19 | CFBundleShortVersionString 20 | $(MARKETING_VERSION) 21 | CFBundleURLTypes 22 | 23 | 24 | CFBundleTypeRole 25 | Editor 26 | CFBundleURLSchemes 27 | 28 | wkfg 29 | 30 | 31 | 32 | CFBundleVersion 33 | 188 34 | LSRequiresIPhoneOS 35 | 36 | UIApplicationSceneManifest 37 | 38 | UIApplicationSupportsMultipleScenes 39 | 40 | UISceneConfigurations 41 | 42 | UIWindowSceneSessionRoleApplication 43 | 44 | 45 | UISceneConfigurationName 46 | Default Configuration 47 | UISceneDelegateClassName 48 | $(PRODUCT_MODULE_NAME).SceneDelegate 49 | UISceneStoryboardFile 50 | Main 51 | 52 | 53 | 54 | 55 | UIApplicationSupportsIndirectInputEvents 56 | 57 | UILaunchStoryboardName 58 | LaunchScreen 59 | UIMainStoryboardFile 60 | Main 61 | UIRequiredDeviceCapabilities 62 | 63 | armv7 64 | 65 | UISupportedInterfaceOrientations 66 | 67 | UIInterfaceOrientationPortrait 68 | UIInterfaceOrientationLandscapeLeft 69 | UIInterfaceOrientationLandscapeRight 70 | 71 | UISupportedInterfaceOrientations~ipad 72 | 73 | UIInterfaceOrientationPortrait 74 | UIInterfaceOrientationPortraitUpsideDown 75 | UIInterfaceOrientationLandscapeLeft 76 | UIInterfaceOrientationLandscapeRight 77 | 78 | 79 | 80 | -------------------------------------------------------------------------------- /Forum/Misc/DataTypes.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Thread.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/9/20. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | protocol DATA { 12 | var id: String {get} 13 | } 14 | 15 | class BaseManager { 16 | var count: Int { 0 } 17 | func initializeCell(_ cell: MainCell, index: Int) -> MainCell { cell } 18 | func clear() -> Self { return self } 19 | func getContent() -> Int { -1 } 20 | func didSelectedRow(_ vc: UIViewController, index: Int, commit: Bool = true) -> UIViewController? { return nil } 21 | } 22 | 23 | class DataManager: BaseManager { 24 | 25 | var data = [T]() 26 | override var count: Int { data.count } 27 | var last = "NULL" 28 | 29 | func networking() -> ([T], String)? { fatalError() } 30 | override func clear() -> Self { 31 | (data, last) = ([], "NULL") 32 | return self 33 | } 34 | override func getContent() -> Int { 35 | if let net = networking() { 36 | data += net.0 37 | last = net.1 38 | return net.0.count 39 | } else { return -1 } 40 | } 41 | 42 | } 43 | 44 | enum LikeState: String { 45 | case none = "hand.thumbsup", like = "hand.thumbsup.fill", disL = "hand.thumbsdown.fill" 46 | static func with(_ i: Int) -> LikeState { 47 | [.disL, .none, .like][i + 1] 48 | } 49 | } 50 | 51 | enum Tag: String, CaseIterable { 52 | case sex = "性相关", politics = "政治相关", uncomfort = "令人不适", unproved = "未经证实", war = "引战" 53 | } 54 | 55 | enum Order: String, CaseIterable { 56 | case earliest = "最早回复", newest = "最新回复", only = "只看洞主", hot = "热度排序" 57 | static let network = [Order.earliest: "0", .newest: "1", .only: "-1", .hot: "2"] 58 | var netStr: String {Self.network[self]!} 59 | } 60 | 61 | struct Thread: DATA { 62 | 63 | enum Category: String, CaseIterable { 64 | case all = "主干道", 65 | sport = "校园", 66 | entertainment = "娱乐", 67 | emotion = "情感", 68 | science = "科学", 69 | it = "数码", 70 | social = "社会", 71 | music = "音乐", 72 | movie = "影视", 73 | art = "文史哲", 74 | life = "人生经验" 75 | } 76 | 77 | var id = "", title = "", content = "", tag: Tag?, folded = true, myTag: Tag?, reported = false 78 | var type: Category = .all 79 | var nLiked = 0, nDislike = 0, nRead = 0, nCommented = 0 80 | var hasLiked = LikeState.like, hasFavoured = false, isTop = false, isFromFloorList = false 81 | var postTime = Date(), lastUpdateTime = Date() 82 | var name: NameG, color: ColorG 83 | 84 | static var cnt = 1 85 | 86 | init(json: Any, isfromFloorList li: Bool = false) { 87 | let thread = json as! [String: Any] 88 | 89 | nCommented = thread["Comment"] as! Int 90 | id = thread["ThreadID"] as! String 91 | nRead = thread["Read"] as! Int 92 | content = thread["Summary"] as! String 93 | nLiked = thread["Like"] as! Int 94 | nDislike = thread["Dislike"] as! Int 95 | title = thread["Title"] as! String 96 | hasLiked = {$0 == nil ? .like : LikeState.with($0!)}(thread["WhetherLike"] as? Int) 97 | hasFavoured = (thread["WhetherFavour"] as? Int ?? 0) == 1 98 | isTop = (thread["WhetherTop"] as? Int) == 1 99 | isFromFloorList = li 100 | 101 | 102 | print(thread.keys) 103 | 104 | tag = Tag.allCases.first(where: {String(describing: $0) == (thread["Tag"] as? String ?? "NULL")}) 105 | myTag = Tag.allCases.first(where: {String(describing: $0) == thread["MyTag"] as? String ?? ""}) 106 | reported = (thread["WhetherReport"] as? Int ?? 0) == 1 107 | 108 | name = NameG( 109 | theme: NameTheme.init(rawValue: thread["AnonymousType"] as! String) ?? .aliceAndBob, 110 | seed: thread["RandomSeed"] as! Int) 111 | color = ColorG(theme: .cold, seed: Int(id)!) 112 | 113 | lastUpdateTime = Util.stringToDate(thread["LastUpdateTime"] as! String) 114 | postTime = Util.stringToDate(thread["PostTime"] as! String) 115 | } 116 | 117 | init(_ s: String? = nil) { 118 | name = NameG(theme: .aliceAndBob, seed: 0) 119 | color = ColorG(theme: .cold, seed: 0) 120 | id = s ?? "" 121 | } 122 | 123 | func setedFolded(_ b: Bool) -> Self { 124 | var res = self; res.folded = b; return res; 125 | } 126 | 127 | func generateFirstFloor() -> Floor { 128 | var f = Floor() 129 | f.name = "0" 130 | f.id = "0" 131 | f.content = content 132 | f.nLiked = nLiked 133 | f.nDisliked = nDislike 134 | f.time = postTime 135 | f.hasLiked = hasLiked 136 | return f 137 | } 138 | 139 | class Manager: DataManager { 140 | 141 | var sortType: Network.NetworkGetThreadType 142 | var block = Thread.Category.all 143 | var searchFor: String? = nil 144 | 145 | var pr = G.viewStyle.content 146 | var filtered = [Thread]() 147 | 148 | override var data: [Thread] { 149 | didSet { 150 | filter() 151 | } 152 | } 153 | 154 | func filter() { 155 | let li = G.blockedList.content 156 | let setting = G.threadStyle.content 157 | pr = G.viewStyle.content 158 | filtered = data.compactMap() { 159 | if sortType == .my || sortType == .favoured { return $0.setedFolded(false) } 160 | 161 | let dis = $0.nLiked - $0.nDislike <= -5 162 | if li.contains($0.id) || (setting == 2 && dis) { return nil } 163 | if $0.tag == nil { return $0.setedFolded($0.folded && setting == 1 && dis) } 164 | switch pr[String(describing: $0.tag!)] ?? 1 { 165 | case 2: return nil 166 | case 1: return $0.setedFolded($0.folded) 167 | default: return $0.setedFolded($0.folded && setting == 1 && dis) 168 | } 169 | } 170 | } 171 | 172 | init(type: Network.NetworkGetThreadType) { 173 | sortType = type 174 | } 175 | 176 | func search(text: String?) { 177 | searchFor = text 178 | } 179 | 180 | func resetSearch() { 181 | searchFor = nil 182 | } 183 | 184 | override func networking() -> ([Thread], String)? { 185 | return searchFor == nil 186 | ? Network.getThreads(type: sortType, inBlock: block, lastSeenID: last) 187 | : Network.searchThreads(keyword: searchFor!, lastSeenID: last) 188 | } 189 | 190 | override var count: Int { 191 | filtered.count 192 | } 193 | 194 | override func initializeCell(_ cell: MainCell, index: Int) -> MainCell { 195 | cell.setAs(thread: index < self.count ? filtered[index] : Thread()) 196 | } 197 | 198 | override func didSelectedRow(_ vc: UIViewController, index: Int, commit: Bool = true) -> UIViewController? { 199 | if filtered[index].folded && commit { 200 | let i = data.firstIndex(where: {$0.id == filtered[index].id})! 201 | data[i].folded = false 202 | (vc as! MainVC).tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) 203 | return nil 204 | } else { 205 | return MainVC.new(.floors, filtered[index])..{ 206 | commit => vc >> $0 207 | } 208 | } 209 | } 210 | 211 | static func openCertainThread(_ vc: UIViewController, id: String) { 212 | MainVC.new(.floors, Thread(id))..{ 213 | vc >> $0 214 | } 215 | } 216 | 217 | } 218 | 219 | } 220 | 221 | struct Floor: DATA { 222 | 223 | var id = "" 224 | var name = "1", content = "" 225 | var nLiked = 0, nDisliked = 0, folded = true, reported = false 226 | var hasLiked = LikeState.none 227 | var time = Date() 228 | 229 | var replyToName: String? 230 | var replyToFloor: Int? 231 | var fake = false 232 | 233 | init(fake: Bool = false) {self.fake = fake} 234 | init(json: Any) { 235 | let floor = json as! [String: Any] 236 | id = floor["FloorID"] as! String 237 | content = floor["Context"] as! String 238 | name = floor["Speakername"] as! String 239 | replyToName = floor["Replytoname"] as? String 240 | replyToFloor = floor["Replytofloor"] as? Int 241 | time = Util.stringToDate(floor["RTime"] as! String) 242 | hasLiked = LikeState.with(floor["WhetherLike"] as! Int) 243 | nLiked = floor["Like"] as! Int 244 | nDisliked = floor["Dislike"] as! Int 245 | reported = (floor["WhetherReport"] as? Int ?? 0) == 1 246 | } 247 | 248 | func setedFolded(_ b: Bool) -> Self { 249 | var res = self; res.folded = b; return res; 250 | } 251 | mutating func setLikeStatus(nLiked nl: Int, nDisliked nd: Int, hasLiked hl: LikeState) { 252 | (nLiked, nDisliked, hasLiked) = (nl, nd, hl) 253 | } 254 | 255 | class Manager: DataManager { 256 | 257 | var thread: Thread 258 | var order = Order.earliest 259 | 260 | var filtered = [Floor]() 261 | override var data: [Floor] { 262 | didSet { 263 | filter() 264 | } 265 | } 266 | 267 | func filter() { 268 | let setting = G.floorStyle.content 269 | filtered = data.compactMap { 270 | if $0.nLiked - $0.nDisliked > -5 { 271 | return $0.setedFolded(false) 272 | } 273 | print(">>>", setting) 274 | switch setting { 275 | case 2: return nil 276 | case 1: return $0.setedFolded($0.folded) 277 | default: return $0.setedFolded(false) 278 | } 279 | } 280 | } 281 | 282 | init(_ t: Thread) { 283 | thread = t 284 | super.init() 285 | } 286 | 287 | override var count: Int {filtered.count + 1} 288 | 289 | override func networking() -> ([Floor], String)? { 290 | Network.getFloors(for: thread.id, lastSeenID: last, order: order.netStr)..{ 291 | if let t = $0.1 { thread = t } 292 | }..\.0 293 | } 294 | 295 | override func initializeCell(_ cell: MainCell, index: Int) -> MainCell { 296 | index == 0 297 | ? cell.setAs(floor: thread.generateFirstFloor(), forThread: thread, firstFloor: true, order: order) 298 | : cell.setAs(floor: index <= filtered.count ? filtered[index - 1] : Floor(fake: true), forThread: thread, firstFloor: false) 299 | } 300 | 301 | func displayNameFor(_ i: Int) -> String { 302 | thread.name[Int((i == 0 ? thread.generateFirstFloor() : filtered.first(where: {$0.id == "\(i)"}) ?? thread.generateFirstFloor()).name)!] 303 | } 304 | 305 | override func didSelectedRow(_ vc: UIViewController, index: Int, commit: Bool = true) -> UIViewController? { 306 | if index > 0, filtered[index - 1].folded { 307 | let i = data.firstIndex(where: {$0.id == filtered[index - 1].id})! 308 | data[i].folded = false 309 | (vc as! MainVC).tableView.reloadRows(at: [IndexPath(row: index, section: 0)], with: .automatic) 310 | } 311 | return nil 312 | } 313 | 314 | } 315 | 316 | } 317 | 318 | struct Message: DATA { 319 | 320 | class Manager: DataManager { 321 | 322 | override func initializeCell(_ cell: MainCell, index: Int) -> MainCell { 323 | cell.setAs(message: index < self.count ? data[index] : Message(Thread())) 324 | } 325 | 326 | override func networking() -> ([Message], String)? { 327 | Network.getMessages(lastSeenID: last) 328 | } 329 | 330 | override func didSelectedRow(_ vc: UIViewController, index: Int, commit: Bool = true) -> UIViewController? { 331 | MainVC.new(.floors, data[index].thread)..{ 332 | commit => vc >> $0 333 | } 334 | } 335 | 336 | } 337 | 338 | var thread: Thread 339 | var ty = 0, judge = 0 340 | var id: String { thread.id } 341 | 342 | init(_ t: Thread) { 343 | self.thread = t 344 | } 345 | 346 | init(json: Any) { 347 | let msg = json as! [String: Any] 348 | print(msg) 349 | thread = Thread(json: json) 350 | ty = msg["Type"] as! Int 351 | judge = msg["Judge"] as! Int 352 | } 353 | 354 | 355 | } 356 | -------------------------------------------------------------------------------- /Forum/Misc/Globals.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Globals.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/10/5. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | 11 | class StoredObject { 12 | var id: String 13 | var nothing: () -> T 14 | init(_ i: String, _ n: @escaping () -> T) { 15 | id = i 16 | nothing = n 17 | } 18 | var content: T { 19 | get { 20 | (UserDefaults.standard.object(forKey: id) as? T) ?? nothing() 21 | } 22 | set(val) { 23 | UserDefaults.standard.setValue(val, forKey: id) 24 | } 25 | } 26 | } 27 | 28 | class G { 29 | 30 | static let token = StoredObject("ForumUserToken", { .init() }) 31 | static let networkStat = StoredObject<[Double]>("ForumNetworkStat", { .init() }) 32 | static func updateStat(_ i: Double) { 33 | var d = G.networkStat.content 34 | if d.count != 5 { 35 | d = [0, 0, 1e10, -1e10, 0] 36 | } 37 | if i > 0 { 38 | d[0] += i 39 | d[1] += 1 40 | d[2] = min(d[2], i) 41 | d[3] = max(d[3], i) 42 | } else { 43 | d[4] += 1 44 | } 45 | G.networkStat.content = d 46 | } 47 | static let numberPerFetch = 8 48 | static let blockedList = StoredObject<[String]>("ForumBlockedList", { .init() }) 49 | static let viewStyle = StoredObject<[String: Int]>("ForumViewStyle", { .init() }) // 0: ok, 1: fold, 2: hide 50 | static let threadStyle = StoredObject("ForumThreadStyle", { 1 }) 51 | static let floorStyle = StoredObject("ForumFloorStyle", { 1 }) 52 | 53 | static var hasLoggedIn: Bool {token.content != ""} 54 | static var blockContent: String? 55 | 56 | static var openThreadID: String? = nil 57 | static var openNewThread: (String?, String?)? = nil 58 | static var updateAvailable: (String, String)? = nil 59 | static var dismissedUpdate = StoredObject("ForumDismissedUpdate", { "" }) 60 | 61 | } 62 | 63 | 64 | -------------------------------------------------------------------------------- /Forum/Misc/Network.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Network.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/9/29. 6 | // 7 | 8 | import Foundation 9 | import Socket 10 | import UIKit 11 | 12 | class Network { 13 | 14 | static let e = JSONEncoder(), ip = "182.254.145.254", port: Int32 = 8080 15 | 16 | static private func connect(_ d: T) -> [String: Any]? { 17 | func singleConnect() -> [String: Any]? { 18 | do { 19 | let data = try e.encode(d) 20 | print(String(data: data, encoding: .utf8)!) 21 | 22 | let s = try Socket.create() 23 | try s.connect(to: ip, port: port, timeout: 10000) 24 | print("connect") 25 | try s.write(from: data) 26 | try s.setReadTimeout(value: 10000) 27 | print("sent") 28 | 29 | usleep(10000) 30 | 31 | var dt = Data() 32 | while try s.read(into: &dt) > 0 { 33 | print("> get") 34 | } 35 | 36 | print("get", String(data: dt, encoding: .utf8)!) 37 | 38 | let rec = try JSONSerialization.jsonObject( 39 | with: dt, 40 | options: .allowFragments 41 | ) 42 | s.close() 43 | 44 | return rec as! [String: Any] 45 | } catch { 46 | return nil 47 | } 48 | } 49 | let before = Date().timeIntervalSince1970 50 | for i in 1...3 { 51 | if let res = singleConnect() { 52 | print("connect success with in \(i) time(s), \((Date().timeIntervalSince1970 - before) * 1000) ms") 53 | G.updateStat((Date().timeIntervalSince1970 - before) * 1000) 54 | return res 55 | } 56 | usleep(10000) 57 | } 58 | G.updateStat(-1) 59 | return nil 60 | } 61 | 62 | static private func getData( 63 | op_code: String, needChecking: Bool = true, 64 | pa_1: String = "0", pa_2: String = "0", pa_3: String = "0", pa_4: String = "0", pa_5: String = "0", pa_6: String = "0", 65 | done: (([String: Any]) -> T) 66 | ) -> T? { 67 | if let result = connect([ 68 | "op_code": op_code, "pa_1": pa_1, "pa_2": pa_2, "pa_3": pa_3, "pa_4": pa_4, "pa_5": pa_5, "pa_6": pa_6, "Token": G.token.content 69 | ]) { 70 | if needChecking, let x = result["login_flag"] as? String, x != "1" { 71 | print("not Authorized", result) 72 | Util.halt() 73 | return nil 74 | } else { 75 | return done(result) 76 | } 77 | } else { 78 | print("FAIL!") 79 | return nil 80 | } 81 | } 82 | 83 | enum NetworkGetThreadType: String { 84 | case time = "1", favoured = "6", my = "7", trending = "d" 85 | } 86 | 87 | static func getThreads(type: NetworkGetThreadType, inBlock: Thread.Category, lastSeenID: String) -> ([Thread], String)? { 88 | getData(op_code: type.rawValue, pa_1: lastSeenID, pa_2: String(Thread.Category.allCases.firstIndex(of: inBlock)!)) { 89 | ( 90 | ($0["thread_list"]! as! [Any]).map { 91 | Thread(json: $0) 92 | }, 93 | $0[$0.keys.first(where: {$0.hasPrefix("LastSeen")})!] as! String 94 | ) 95 | } 96 | } 97 | 98 | static func searchThreads(keyword: String, lastSeenID: String) -> ([Thread], String)? { 99 | getData(op_code: "b", pa_1: keyword, pa_2: lastSeenID) { 100 | ( 101 | ($0["thread_list"]! as! [Any]).map { 102 | Thread(json: $0) 103 | }, 104 | $0[$0.keys.first(where: {$0.hasPrefix("LastSeen")})!] as! String 105 | ) 106 | } 107 | } 108 | 109 | static func getFloors(for threadID: String, lastSeenID: String, order: String) -> (([Floor], String)?, Thread?) { 110 | getData(op_code: "2", pa_1: threadID, pa_2: lastSeenID, pa_3: order) { 111 | $0["ExistFlag"] as! String == "0" ? (nil, nil) : 112 | ( 113 | ( 114 | ($0["floor_list"]! as! [Any]).map {Floor(json: $0)}, 115 | $0[$0.keys.first(where: {$0.hasPrefix("LastSeen")})!] as! String 116 | ), 117 | Thread(json: $0["this_thread"]!, isfromFloorList: true) 118 | ) 119 | } ?? (nil, nil) 120 | } 121 | 122 | static func getMessages(lastSeenID: String) -> ([Message], String)? { 123 | getData(op_code: "a", pa_1: lastSeenID) { 124 | ( 125 | ($0["message_list"]! as! [Any]).map { 126 | Message(json: $0) 127 | }, 128 | $0[$0.keys.first(where: {$0.hasPrefix("LastSeen")})!] as! String 129 | ) 130 | } 131 | } 132 | 133 | static func verifyToken() -> (String, String)? { 134 | getData(op_code: "-1", needChecking: false) { 135 | let release = ($0["ReleaseTime"] as? String) ?? "" 136 | let thread = ($0["Ban_ThreadID"] as? String) ?? "" 137 | let content = ($0["Ban_Content"] as? String) ?? "" 138 | let reason = ($0["Ban_Reason"] as? String) ?? "" 139 | let isReply = (($0["Ban_Style"] as? String) ?? "") == "1" 140 | return ($0["login_flag"]! as! String, "由于\(isReply ? "你在 #\(thread) 下的回复" : "你的发帖 #\(thread)") \(reason) ,已被我们屏蔽。结合你之前在无可奉告的封禁记录,你的账号将被暂时封禁至 \(release)。\n\n违规\(isReply ? "回复" : "发帖")内容为:\n\(content)\n\n请与我们一起维护无可奉告社区环境。\n谢谢!") 141 | } 142 | } 143 | 144 | static func requestLogin(with email: String) -> Bool { 145 | getData(op_code: "0", needChecking: false, pa_1: email) { 146 | $0["VarifiedEmailAddress"] as! Int == 1 147 | } ?? false 148 | } 149 | 150 | static func performLogin(with email: String, verificationCode: String) -> (Bool, String) { 151 | getData(op_code: "f", needChecking: false, pa_1: email, pa_2: verificationCode, pa_3: UIDevice.current.identifierForVendor!.uuidString) { 152 | ($0["login_flag"] as! Int == 0, $0["Token"] as! String) 153 | } ?? (false, "") 154 | } 155 | 156 | static func favourThread(for threadID: String) -> Bool { 157 | getData(op_code: "5", pa_1: threadID, done: {_ in true}) ?? false 158 | } 159 | 160 | static func cancelFavourThread(for threadID: String) -> Bool { 161 | getData(op_code: "5_2", pa_1: threadID, done: {_ in true}) ?? false 162 | } 163 | 164 | static func likeFloor(for threadID: String, floor: String) -> Bool { 165 | getData(op_code: "8", pa_1: threadID, pa_4: floor, done: {_ in true}) ?? false 166 | } 167 | 168 | static func cancelLikeFloor(for threadID: String, floor: String) -> Bool { 169 | getData(op_code: "8_2", pa_1: threadID, pa_4: floor, done: {_ in true}) ?? false 170 | } 171 | 172 | static func dislikeFloor(for threadID: String, floor: String) -> Bool { 173 | getData(op_code: "8_5", pa_1: threadID, pa_4: floor, done: {_ in true}) ?? false 174 | } 175 | 176 | static func canceldislikeFloor(for threadID: String, floor: String) -> Bool { 177 | getData(op_code: "8_6", pa_1: threadID, pa_4: floor, done: {_ in true}) ?? false 178 | } 179 | 180 | static func likeThread(for threadID: String) -> Bool { 181 | getData(op_code: "8_3", pa_1: threadID, done: {_ in true}) ?? false 182 | } 183 | 184 | static func cancelLikeThread(for threadID: String) -> Bool { 185 | getData(op_code: "8_4", pa_1: threadID, done: {_ in true}) ?? false 186 | } 187 | 188 | static func dislikeThread(for threadID: String) -> Bool { 189 | getData(op_code: "9", pa_1: threadID, done: {_ in true}) ?? false 190 | } 191 | 192 | static func cancelDislikeThread(for threadID: String) -> Bool { 193 | getData(op_code: "9_2", pa_1: threadID, done: {_ in true}) ?? false 194 | } 195 | 196 | static func reportThread(for threadID: String) -> Bool { 197 | getData(op_code: "e", pa_1: threadID, done: {_ in true}) ?? false 198 | } 199 | 200 | static func reportFloor(for threadID: String, floor: String) -> Bool { 201 | getData(op_code: "h", pa_1: threadID, pa_2: floor, done: {_ in true}) ?? false 202 | } 203 | 204 | static func setTag(for threadID: String, with tag: Tag) -> Bool { 205 | getData(op_code: "i", pa_1: threadID, pa_4: String(describing: tag), done: {_ in true}) ?? false 206 | } 207 | 208 | static func newThread(title: String, inBlock: Thread.Category, content: String, anonymousType: NameTheme, seed: Int, tag: Tag?) -> Bool { 209 | return getData(op_code: "3", pa_1: title, pa_2: String(Thread.Category.allCases.firstIndex(of: inBlock)!), pa_3: content, pa_4: anonymousType.rawValue, pa_5: String(seed), pa_6: tag == nil ? "NULL" : String(describing: tag!), done: {_ in true}) ?? false 210 | } 211 | 212 | static func newReply(for threadID: String, floor: String, content: String) -> Bool { 213 | getData(op_code: floor == "0" ? "4" : "4_2", pa_1: threadID, pa_3: content, pa_4: floor, done: {_ in true}) ?? false 214 | } 215 | 216 | } 217 | -------------------------------------------------------------------------------- /Forum/Misc/Utilities.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Utilities.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/10/20. 6 | // 7 | 8 | import Foundation 9 | import UIKit 10 | import MBProgressHUD 11 | import Material 12 | import DropDown 13 | 14 | class Util { 15 | 16 | static let formatter = DateFormatter()..{ 17 | $0.dateFormat = "yyyy-MM-dd HH:mm:ss" 18 | } 19 | 20 | static func dateToString(_ date: Date) -> String { 21 | formatter.string(from: date) 22 | } 23 | 24 | static let trans: [((DateComponents) -> Int?, String)] = [ 25 | ({$0.year}, "年"), ({$0.month}, "月"), ({$0.day}, "天"), ({$0.hour}, "小时"), ({$0.minute}, "分钟") 26 | ] 27 | 28 | static func dateToDeltaString(_ date: Date) -> String { 29 | let interval = Calendar.current.dateComponents([.year, .month, .day, .hour, .minute], from: date, to: Date()) 30 | for (f, s) in trans { 31 | if let n = f(interval), n > 0 { 32 | return "\(n)\(s)前" 33 | } 34 | } 35 | return "刚刚" 36 | } 37 | 38 | static func stringToDate(_ string: String) -> Date { 39 | formatter.date(from: string)! 40 | } 41 | 42 | static func halt() { 43 | DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) { 44 | UIApplication.shared.perform(#selector(NSXPCConnection.suspend)) 45 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { 46 | exit(0) 47 | } 48 | } 49 | } 50 | 51 | } 52 | 53 | extension UIImage { 54 | 55 | @available(iOS, obsoleted: 13.0) 56 | convenience init?(systemName name: String, compatibleWith traitCollection: UITraitCollection?) { 57 | self.init() 58 | } 59 | 60 | } 61 | 62 | extension UIColor { 63 | 64 | @available(iOS, obsoleted: 13.0) 65 | class var systemBackground: UIColor { 66 | .white 67 | } 68 | 69 | } 70 | 71 | class DarkSupportTextField: TextField { 72 | override func prepare() { 73 | super.prepare() 74 | textColor = self.traitCollection.userInterfaceStyle == .dark ? .lightText : .darkText 75 | } 76 | } 77 | 78 | @discardableResult func with(_ value: T, _ block: (T) -> Void) -> T 79 | { 80 | block(value) 81 | return value 82 | } 83 | 84 | final class Action { 85 | private let _action: () -> () 86 | init(_ action: @escaping () -> ()) { _action = action; } 87 | @objc func action() { _action() } 88 | } 89 | 90 | precedencegroup LowestPrecedence { 91 | associativity: left 92 | lowerThan: AssignmentPrecedence 93 | } 94 | infix operator ..: MultiplicationPrecedence 95 | 96 | @discardableResult func .. (_ lhs: T, _ rhs: (T) -> Void) -> T { 97 | rhs(lhs) 98 | return lhs 99 | } 100 | @discardableResult func .. (_ lhs: (T, K), _ rhs: (T, K) -> Void) -> (T, K) { 101 | rhs(lhs.0, lhs.1) 102 | return lhs 103 | } 104 | @discardableResult func .. (_ lhs: T, _ rhs: KeyPath) -> K { 105 | return lhs[keyPath: rhs] 106 | } 107 | 108 | infix operator => : LowestPrecedence 109 | func => (_ lhs: @autoclosure () -> Bool, _ rhs: @autoclosure () -> Void) { 110 | if lhs() { rhs() } 111 | } 112 | func => (_ lhs: @autoclosure () -> Bool, _ rhs: () -> Void) { 113 | if lhs() { rhs() } 114 | } 115 | func => (_ lhs: () -> Bool, _ rhs: () -> Void) { 116 | if lhs() { rhs() } 117 | } 118 | func += (_ lhs: UIView, _ rhs: UIView) { 119 | lhs.addSubview(rhs) 120 | } 121 | 122 | prefix operator * 123 | prefix func * (block: @escaping () -> ()) -> Selector { 124 | #selector(Action(block).action) 125 | } 126 | 127 | precedencegroup SecondaryTernaryPrecedence { 128 | associativity: right 129 | higherThan: TernaryPrecedence 130 | lowerThan: LogicalDisjunctionPrecedence 131 | } 132 | infix operator ?> : SecondaryTernaryPrecedence 133 | infix operator ?< : TernaryPrecedence 134 | 135 | func ?> (lhs: @autoclosure () -> Bool, rhs: @escaping @autoclosure () -> T) -> (Bool, () -> T) { return (lhs(), rhs) } 136 | func ?> (lhs: () -> Bool, rhs: @escaping () -> T) -> (Bool, () -> T) { return (lhs(), rhs) } 137 | @discardableResult func ?< (lhs: (Bool, () -> T), rhs: @escaping @autoclosure () -> T) -> T { lhs.0 ? lhs.1() : rhs() } 138 | @discardableResult func ?< (lhs: (Bool, () -> T), rhs: @escaping () -> T) -> T { lhs.0 ? lhs.1() : rhs() } 139 | 140 | // MARK: - Generator 141 | 142 | extension Array { 143 | mutating func shuffle(seed s: Int) { 144 | let random = RandomN(s) 145 | for i in 1.. Int { 156 | if seed == 0 { 157 | a += 1 158 | return a 159 | } else { 160 | var t = a, s = b 161 | a = s 162 | t ^= t << 23; 163 | t ^= t >> 17; 164 | t ^= s ^ (s >> 26); 165 | b = t 166 | return (s &+ t) & (Int.max) // allow overflow 167 | } 168 | } 169 | } 170 | 171 | protocol ProvideList: Hashable { 172 | associatedtype T 173 | static var list: [Self: [T]] { get } 174 | } 175 | 176 | class Generator { 177 | var vals: [K.T] 178 | 179 | init(theme t: K, seed s: Int) { 180 | vals = K.list[t]! 181 | vals.shuffle(seed: s) 182 | } 183 | subscript(i: Int) -> K.T { 184 | vals[i % vals.count] 185 | } 186 | } 187 | 188 | 189 | enum NameTheme: String, CaseIterable, ProvideList { 190 | case aliceAndBob = "abc", usPresident = "us_president", tarot = "tarot" 191 | static let displayName = [ 192 | aliceAndBob: "Alice and Bob", usPresident: "US President", tarot: "Tarot" 193 | ] 194 | static let list: [NameTheme: [String]] = [ 195 | .aliceAndBob: ["Alice", "Bob", "Carol", "Dave", "Eve", "Forest", "George", "Harry", "Issac", "Justin", "Kevin", "Laura", "Mallory", "Neal", "Oscar", "Pat", "Quentin", "Rose", "Steve", "Trent", "Utopia", "Victor", "Walter", "Xavier", "Young", "Zoe"], 196 | .usPresident: ["Washington", "J.Adams", "Jefferson", "Madison", "Monroe", "J.Q.Adams", "Jackson", "Buren", "W.H.Harrison", "J.Tyler", "Polk", "Z.Tylor", "Fillmore", "Pierce", "Buchanan", "Lincoln", "A.Johnson", "Grant", "Hayes", "Garfield", "Arthur", "Cleveland", "B.Harrison", "McKinley", "T.T.Roosevelt","Taft", "Wilson", "Harding", "Coolidge", "Hoover", "F.D.Roosevelt", "Truman", "Eisenhower", "Kennedy", "L.B.Johnson", "Nixon", "Ford", "Carter", "Reagan", "G.H.W.Bush", "Clinton","G.W.Bush", "Obama", "Trump"], 197 | // .tarot: ["愚者", "魔术师", "女祭司", "皇后", "皇帝", "教皇", "恋人", "战车", "力量", "隐者", "命运之轮", "正义", "倒吊人", "死神", "节制", "恶魔", "塔", "星星", "月亮", "太阳", "审判", "世界"] 198 | .tarot: ["The Fool", "The Magician", "The High Priestess", "The Empress", "The Emperor", "The Hierophant", "The Lovers", "The Chariot", "Justice", "The Hermit", "Wheel of Fortune", "Strength", "The Hanged Man", "Death", "Temperance", "The Devil", "The Tower", "The Star", "The Moon", "The Sun", "Judgement", "The World"] 199 | ] 200 | var displayText: String {Self.displayName[self]!} 201 | } 202 | 203 | enum ColorTheme: ProvideList { 204 | case cold 205 | static let list: [ColorTheme : [UIColor]] = [ 206 | .cold: [0x5ebd3e, 0xffb900, 0xf78200, 0xe23838, 0x973999, 0x009cdf] 207 | ].mapValues { 208 | $0.map { 209 | UIColor(argb: $0 + (0xc0 << 24)) 210 | } 211 | } 212 | } 213 | 214 | class NameG: Generator { 215 | override subscript(i: Int) -> String { 216 | (i >= vals.count 217 | ? "\(vals[i % vals.count]).\(i / vals.count + 1)" 218 | : vals[i]) 219 | // + (i == 0 ? "(洞主)" : "") 220 | } 221 | } 222 | 223 | class ColorG: Generator { 224 | } 225 | 226 | //class ColorG: Generator 227 | 228 | // MARK: - Navigation Utilities 229 | 230 | 231 | protocol DoubleTappable { 232 | func hasTappedAgain() 233 | } 234 | 235 | extension String { 236 | static prefix func * (name: String) -> UIViewController { 237 | UIStoryboard(name: "Main", bundle: nil) 238 | .instantiateViewController(identifier: name) 239 | } 240 | 241 | var linebreaks: Int { 242 | self.reduce(1) { 243 | $0 + ($1 == "\n" ? 1 : 0) 244 | } 245 | } 246 | } 247 | 248 | extension UIViewController { 249 | static func >> (_ vc: UIViewController, _ to: UIViewController) { 250 | vc.navigationController?.pushViewController(to, animated: true) 251 | } 252 | 253 | static func << (_ vc: UIViewController, _ to: UIViewController) { 254 | vc.present(to, animated: true, completion: nil) 255 | } 256 | 257 | func topMostViewController() -> UIViewController { 258 | 259 | if let presented = self.presentedViewController { 260 | return presented.topMostViewController() 261 | } 262 | 263 | if let navigation = self as? UINavigationController { 264 | return navigation.visibleViewController?.topMostViewController() ?? navigation 265 | } 266 | 267 | if let tab = self as? UITabBarController { 268 | return tab.selectedViewController?.topMostViewController() ?? tab 269 | } 270 | 271 | return self 272 | } 273 | } 274 | 275 | extension String { 276 | 277 | func fromBase64() -> String? { 278 | guard let data = Data(base64Encoded: self) else { 279 | return nil 280 | } 281 | 282 | return String(data: data, encoding: .utf8) 283 | } 284 | 285 | func toBase64() -> String { 286 | return Data(self.utf8).base64EncodedString() 287 | } 288 | 289 | } 290 | 291 | func dealWithURLContext(_ urlContext: UIOpenURLContext) { 292 | let url = urlContext.url.absoluteString.replacingOccurrences(of: "wkfg://", with: "") 293 | print("url = \(url)") 294 | 295 | if let i = Int(url), i >= 1, i < 1000000 { 296 | G.openThreadID = url 297 | } else { 298 | G.openThreadID = nil 299 | if url.hasPrefix("newThread") { 300 | G.openNewThread = (nil, url.replacingOccurrences(of: "newThread/", with: "").fromBase64() ?? "url") 301 | } 302 | } 303 | 304 | } 305 | 306 | func isUpdateAvailable() -> (Bool, String, String)? { 307 | guard let info = Bundle.main.infoDictionary, 308 | let currentVersion = info["CFBundleShortVersionString"] as? String, 309 | let identifier = info["CFBundleIdentifier"] as? String, 310 | let url = URL(string: "http://itunes.apple.com/lookup?bundleId=\(identifier)") else { 311 | return nil 312 | } 313 | do { 314 | let data = try Data(contentsOf: url) 315 | guard let json = try JSONSerialization.jsonObject(with: data, options: [.allowFragments]) as? [String: Any] else { 316 | return nil 317 | } 318 | if let result = (json["results"] as? [Any])?.first as? [String: Any], let version = result["version"] as? String { 319 | return (version != currentVersion, version, currentVersion) 320 | } 321 | } catch {} 322 | return nil 323 | } 324 | 325 | extension UIBarButtonItem { 326 | 327 | static func imgItem(_ image: UIImage?, action: @autoclosure () -> Selector, to vc: UIViewController) -> UIBarButtonItem { 328 | UIBarButtonItem(customView: UIButton(frame: CGRect(x: 0, y: 0, width: image!.width, height: image!.height))..{ 329 | $0.setBackgroundImage(image, for: .normal) 330 | $0.addTarget(vc, action: action(), for: .touchUpInside) 331 | }) 332 | } 333 | 334 | func setImageTo(_ image: UIImage?) { 335 | (customView as! UIButton).setBackgroundImage(image, for: .normal) 336 | } 337 | 338 | } 339 | 340 | // MARK: - View Style 341 | 342 | extension UIView { 343 | func applyCardStyle(clip: Bool = false) { 344 | self.backgroundColor = .systemBackground 345 | self.layer.cornerRadius = 7 346 | self.layer.masksToBounds = clip 347 | self.layer.backgroundColor = UIColor.tertiarySystemBackground.cgColor 348 | // if traitCollection.userInterfaceStyle != .dark { 349 | // self.layer.shadowColor = UIColor.label.cgColor 350 | // self.layer.shadowOffset = CGSize(width: 0, height: 1); 351 | // self.layer.shadowOpacity = 0.1 352 | // self.layer.shadowRadius = 2 353 | // self.layer.borderColor = UIColor.systemBackground.cgColor 354 | // } 355 | } 356 | func applyShadow(opaque: Bool = true, offset: Double = 4, opacity: Float = 0.05) { 357 | if traitCollection.userInterfaceStyle == .dark { 358 | return 359 | } 360 | opaque => self.layer.backgroundColor = UIColor.systemBackground.cgColor 361 | self.layer.shadowColor = UIColor.label.cgColor 362 | self.layer.shadowOffset = CGSize(width: 0, height: offset); 363 | self.layer.shadowOpacity = opacity 364 | } 365 | } 366 | 367 | extension UILabel { 368 | func setAsTagLabel(_ t: String) -> Self { 369 | text = t 370 | fontSize = 14 371 | textColor = .white 372 | textAlignment = .center 373 | layer.cornerRadius = 5 374 | layer.backgroundColor = UIColor(rgb: 0x018786).cgColor 375 | return self 376 | } 377 | } 378 | 379 | extension UIButton { 380 | func setDropDownStyle(fontSize: CGFloat = 12) { 381 | self.contentHorizontalAlignment = .right 382 | self.setTitleColor(UIColor(named: "AccentColor"), for: .normal) 383 | self.setImage(UIImage(systemName: "chevron.compact.down", withConfiguration: UIImage.SymbolConfiguration(scale: .small)), for: .normal) 384 | self.semanticContentAttribute = .forceRightToLeft 385 | self.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) 386 | self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 5) 387 | self.titleLabel!.font = UIFont.systemFont(ofSize: fontSize) 388 | } 389 | } 390 | 391 | func updateCountingLabel(label: StateLabel, text: String, lineLimit: Int, charLimit: Int) { 392 | let lc = text.linebreaks, cc = text.count 393 | let line = NSAttributedString(string: "\(lc)/\(lineLimit) 行\t", attributes: [NSAttributedString.Key.foregroundColor: lc > lineLimit ? UIColor.red : UIColor.gray]) 394 | let char = NSMutableAttributedString(string: "\(cc)/\(charLimit) 字", attributes: [NSAttributedString.Key.foregroundColor: cc > charLimit ? UIColor.red : UIColor.gray]) 395 | if lineLimit > 1 { 396 | char.insert(line, at: 0) 397 | } 398 | label.attributedText = char 399 | label.ok = lc <= lineLimit && cc <= charLimit 400 | } 401 | 402 | 403 | class CheckerButton: UIButton { 404 | var checked = false { 405 | didSet { 406 | self.setImage(UIImage(systemName: checked ? "checkmark.square" : "squareshape", withConfiguration: UIImage.SymbolConfiguration(scale: .small)), for: .normal) 407 | } 408 | } 409 | 410 | func setCheckBoxStyle(fontSize: CGFloat = 12) { 411 | self.contentHorizontalAlignment = .right 412 | self.setTitleColor(UIColor(named: "AccentColor"), for: .normal) 413 | self.semanticContentAttribute = .forceRightToLeft 414 | self.titleEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 10) 415 | self.imageEdgeInsets = UIEdgeInsets(top: 0, left: 0, bottom: 0, right: 5) 416 | self.titleLabel!.font = UIFont.systemFont(ofSize: fontSize) 417 | self.addTarget(self, action: #selector(checked(_:)), for: .touchUpInside) 418 | checked = false 419 | } 420 | 421 | @objc func checked(_ sender: UIButton) { 422 | checked = !checked 423 | } 424 | } 425 | 426 | class StateLabel: UILabel { 427 | var ok = true 428 | } 429 | 430 | extension UIViewController { 431 | class BiggerImageView: UIImageView { 432 | override var intrinsicContentSize: CGSize { 433 | image!.size.applying(.init(scaleX: 2.0, y: 2.0)) 434 | } 435 | } 436 | 437 | enum AlertStyle: String { 438 | case success = "checkmark.circle", failure = "xmark.octagon", warning = "exclamationmark.triangle" 439 | } 440 | func setAndHideAlert(_ bar: MBProgressHUD, _ message: String, style: AlertStyle, duration: TimeInterval = 0.8, completion: @escaping () -> Void = {}) { 441 | DispatchQueue.main.async { 442 | bar.mode = .customView 443 | bar.customView = BiggerImageView(image: UIImage(systemName: style.rawValue, withConfiguration: UIImage.SymbolConfiguration(scale: .large))) 444 | bar.label.text = message 445 | bar.completionBlock = completion 446 | bar.hide(animated: true, afterDelay: duration) 447 | } 448 | } 449 | func showAlert(_ message: String, style: AlertStyle, duration: TimeInterval = 0.8, completion: @escaping () -> Void = {}) { 450 | setAndHideAlert(MBProgressHUD.showAdded(to: self.view, animated: true), message, style: style, duration: duration, completion: completion) 451 | } 452 | func networkFailure(completion: @escaping () -> Void = {}) { 453 | showAlert("网络错误", style: .failure, completion: completion) 454 | } 455 | func showProgress() -> MBProgressHUD { 456 | MBProgressHUD.showAdded(to: self.view, animated: true)..{ 457 | $0.mode = .indeterminate 458 | } 459 | } 460 | } 461 | -------------------------------------------------------------------------------- /Forum/OtherViews/AboutVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/11/9. 6 | // 7 | 8 | import UIKit 9 | 10 | class AboutVC: UIViewController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | addKeyCommand(.init(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(esc))) 17 | } 18 | 19 | private var fatherVC: BaseTableVC! 20 | func withFather(_ vc: BaseTableVC) -> Self { 21 | fatherVC = vc 22 | return self 23 | } 24 | 25 | override func viewWillDisappear(_ animated: Bool) { 26 | super.viewWillDisappear(animated) 27 | if isBeingDismissed { 28 | fatherVC.deselect() 29 | } 30 | } 31 | 32 | @IBAction func dismissClicked(_ sender: Any) { 33 | // fatherVC.deselect() 34 | dismiss(animated: true, completion: nil) 35 | } 36 | 37 | /* 38 | // MARK: - Navigation 39 | 40 | // In a storyboard-based application, you will often want to do a little preparation before navigation 41 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 42 | // Get the new view controller using segue.destination. 43 | // Pass the selected object to the new view controller. 44 | } 45 | */ 46 | 47 | } 48 | -------------------------------------------------------------------------------- /Forum/OtherViews/LoginVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // LoginViewController.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/10/5. 6 | // 7 | 8 | import UIKit 9 | 10 | class LoginVC: UIViewController { 11 | 12 | @IBOutlet weak var emailTextField: UITextField! 13 | @IBOutlet weak var codeTextField: UITextField! 14 | @IBOutlet weak var navBar: UINavigationBar! 15 | @IBOutlet weak var iconImage: UIImageView! 16 | 17 | var sentEmail = "" 18 | var isBase = false 19 | 20 | @IBAction func dismissBtnClicked(_ sender: Any) { 21 | dismiss(animated: true, completion: nil) 22 | } 23 | 24 | override func viewDidLoad() { 25 | super.viewDidLoad() 26 | // Do any additional setup after loading the view. 27 | isBase = presentingViewController == nil 28 | if traitCollection.userInterfaceStyle == .dark { 29 | if let filter = CIFilter(name: "CIColorInvert") { 30 | filter.setValue(CIImage(image: iconImage.image!), forKey: kCIInputImageKey) 31 | let newImage = UIImage(ciImage: filter.outputImage!) 32 | iconImage.image = newImage 33 | } 34 | } 35 | isBase => navBar.isHidden = true 36 | } 37 | 38 | @IBAction func sendVerificationCode(_ sender: Any) { 39 | if let email = emailTextField.text, email.hasSuffix("@sjtu.edu.cn") { 40 | showProgress()..{ bar in 41 | DispatchQueue.global().async { 42 | if Network.requestLogin(with: email) { 43 | self.setAndHideAlert(bar, "验证码发送成功", style: .success); 44 | self.sentEmail = email 45 | } else { self.setAndHideAlert(bar, "验证码发送失败", style: .failure) } 46 | } 47 | } 48 | } else { showAlert("请填写正确的交大邮箱", style: .warning) } 49 | } 50 | 51 | @IBAction func loginBtnClicked(_ sender: Any) { 52 | if let code = codeTextField.text, code != "" { 53 | if sentEmail != "" { 54 | let bar = showProgress() 55 | DispatchQueue.global().async { 56 | print("try........................................") 57 | let (success, token) = Network.performLogin(with: self.sentEmail, verificationCode: code) 58 | print("end........................................", success, token) 59 | if success { 60 | self.setAndHideAlert(bar, "验证成功", style: .success) { 61 | G.token.content = token 62 | print("Success! token = \(token)") 63 | if self.isBase { 64 | let vc = *"InitTabVC" 65 | vc.modalPresentationStyle = .fullScreen 66 | self.present(vc, animated: true, completion: nil) 67 | } else { 68 | self.dismiss(animated: true, completion: nil) 69 | } 70 | } 71 | } else { self.setAndHideAlert(bar, "验证失败", style: .failure) } 72 | } 73 | } else { showAlert("请先发送验证码", style: .warning) } 74 | } else { showAlert("请填写验证码", style: .warning) } 75 | } 76 | /* 77 | // MARK: - Navigation 78 | 79 | // In a storyboard-based application, you will often want to do a little preparation before navigation 80 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 81 | // Get the new view controller using segue.destination. 82 | // Pass the selected object to the new view controller. 83 | } 84 | */ 85 | 86 | } 87 | -------------------------------------------------------------------------------- /Forum/OtherViews/MiscTableViewCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutTableViewCell.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/10/31. 6 | // 7 | 8 | import UIKit 9 | 10 | class MiscTableViewCell: UITableViewCell { 11 | 12 | @IBOutlet weak var icon: UIImageView! 13 | 14 | override func awakeFromNib() { 15 | super.awakeFromNib() 16 | // Initialization code 17 | } 18 | 19 | override func setSelected(_ selected: Bool, animated: Bool) { 20 | super.setSelected(selected, animated: animated) 21 | 22 | // Configure the view for the selected state 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /Forum/OtherViews/MiscVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // AboutViewController.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/10/5. 6 | // 7 | 8 | import UIKit 9 | 10 | protocol ProvideContent { 11 | var content: [[(title: String, fun: () -> Void)]] { get } 12 | } 13 | 14 | class BaseTableVC: UIViewController, UITableViewDataSource, UITableViewDelegate, ProvideContent { 15 | 16 | var _tableView: UITableView! { nil } 17 | var content: [[(title: String, fun: () -> Void)]] { [] } 18 | var cellName: String { "" } 19 | 20 | func numberOfSections(in tableView: UITableView) -> Int { 21 | content.count 22 | } 23 | 24 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 25 | content[section].count 26 | } 27 | 28 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 29 | section == 0 ? 40 : 0 30 | } 31 | 32 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 33 | let cell = tableView.dequeueReusableCell(withIdentifier: cellName, for: indexPath) 34 | cell.textLabel?.text = content[indexPath.section][indexPath.row].title 35 | return cell 36 | } 37 | 38 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 39 | 50 40 | } 41 | 42 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 43 | content[indexPath.section][indexPath.row].fun() 44 | } 45 | 46 | override func viewDidLoad() { 47 | super.viewDidLoad() 48 | 49 | // Do any additional setup after loading the view. 50 | _tableView.delegate = self 51 | _tableView.dataSource = self 52 | _tableView.tableFooterView = UIView(frame: CGRect.zero) 53 | _tableView.contentInsetAdjustmentBehavior = .never 54 | navigationController?.navigationBar.prefersLargeTitles = true 55 | navigationItem.largeTitleDisplayMode = .always 56 | } 57 | 58 | func deselect() { 59 | if let selectionIndexPath = self._tableView.indexPathForSelectedRow { 60 | self._tableView.deselectRow(at: selectionIndexPath, animated: true) 61 | } 62 | } 63 | 64 | override func viewWillAppear(_ animated: Bool) { 65 | super.viewWillAppear(animated) 66 | deselect() 67 | } 68 | 69 | } 70 | 71 | class MiscVC: BaseTableVC, UIPopoverPresentationControllerDelegate { 72 | 73 | @IBOutlet weak var tableView: UITableView! 74 | 75 | lazy var misc_content: [[(title: String, fun: () -> Void)]] = [ 76 | [ 77 | ("通知", {self >> MainVC.new(.messages)}), 78 | ("收藏", {self >> MainVC.new(.favour)}) 79 | ], 80 | [ 81 | ("我的帖子", {self >> MainVC.new(.my)}) 82 | ], 83 | [ 84 | ("设置", {self >> *"SettingVC"}), 85 | ("反馈", {self >> *"ReportVC"}) 86 | ] 87 | ] 88 | override var content: [[(title: String, fun: () -> Void)]] { misc_content } 89 | override var cellName: String { "MiscCell" } 90 | override var _tableView: UITableView! { tableView } 91 | 92 | func adaptivePresentationStyle(for controller: UIPresentationController) -> UIModalPresentationStyle { 93 | .none 94 | } 95 | 96 | } 97 | 98 | class SettingVC: BaseTableVC { 99 | 100 | @IBOutlet weak var tableView: UITableView! 101 | 102 | lazy var setting_content: [[(title: String, fun: () -> Void)]] = [ 103 | [("被踩较多的帖子", {})] + Tag.allCases.map { 104 | ($0.rawValue, {}) 105 | }, 106 | [ 107 | ("被踩较多的回复", {}) 108 | ], 109 | [ 110 | ("网络统计", {self >> *"TokenVC"}), 111 | ("关于", {self >> *"AboutMenuVC"}), 112 | ("退出登录", { 113 | G.token.content = "" 114 | self.showAlert("成功退出登录,App即将关闭", style: .success) { 115 | Util.halt() 116 | } 117 | }) 118 | ] 119 | ] 120 | override func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 121 | let cell = tableView.dequeueReusableCell(withIdentifier: cellName, for: indexPath) as! SettingCell 122 | cell.textLabel?.text = content[indexPath.section][indexPath.row].title 123 | let pr = G.viewStyle.content 124 | if indexPath.section == 0 { 125 | if indexPath.row > 0 { 126 | cell.forTag = Tag.allCases[indexPath.row - 1] 127 | cell.segment.selectedSegmentIndex = pr[String(describing: cell.forTag!)] ?? 0 128 | } else { 129 | cell.segment.selectedSegmentIndex = G.threadStyle.content 130 | } 131 | cell.selectionStyle = .none 132 | } else if indexPath.section == 1 { 133 | cell.selectionStyle = .none 134 | cell.segment.selectedSegmentIndex = G.floorStyle.content 135 | cell.forThread = false 136 | } else { 137 | cell.segment.isHidden = true 138 | } 139 | return cell 140 | } 141 | 142 | func tableView(_ tableView: UITableView, titleForHeaderInSection section: Int) -> String? { 143 | if section == 0 { 144 | return "帖子显示选项" 145 | } else if section == 1 { 146 | return "楼层显示选项" 147 | } else { 148 | return nil 149 | } 150 | } 151 | override var content: [[(title: String, fun: () -> Void)]] { setting_content } 152 | override var cellName: String { "SettingCell" } 153 | override var _tableView: UITableView! { tableView } 154 | 155 | } 156 | 157 | class AboutMenuVC: BaseTableVC { 158 | 159 | @IBOutlet weak var tableView: UITableView! 160 | 161 | lazy var setting_content: [[(title: String, fun: () -> Void)]] = [ 162 | [ 163 | ("主页", {UIApplication.shared.open(URL(string: "http://wukefenggao.cn")!)}), 164 | ("社区规范", {UIApplication.shared.open(URL(string: "http://wukefenggao.cn/code")!)}), 165 | ("无可奉告之禅", {self << (*"AboutVC" as! AboutVC).withFather(self)}) 166 | ] 167 | ] 168 | override var content: [[(title: String, fun: () -> Void)]] { setting_content } 169 | override var cellName: String { "AboutMenuCell" } 170 | override var _tableView: UITableView! { tableView } 171 | 172 | } 173 | 174 | class TokenVC: UIViewController { 175 | 176 | @IBOutlet weak var textView: UITextView! 177 | 178 | override func viewDidLoad() { 179 | super.viewDidLoad() 180 | 181 | self.navigationController?.navigationBar.prefersLargeTitles = true 182 | navigationItem.largeTitleDisplayMode = .always 183 | 184 | let d = G.networkStat.content 185 | textView.text = "Failed: \(Int(d[4]))\n Average: \(d[0] / d[1])ms\n Min: \(d[2])ms\n Max: \(d[3])ms" 186 | 187 | } 188 | 189 | } 190 | 191 | class ReportVC: UIViewController { 192 | 193 | @IBOutlet weak var threadID: DarkSupportTextField! 194 | @IBOutlet weak var textView: UITextView! 195 | 196 | 197 | @IBAction func report(_ sender: Any) { 198 | if (threadID.text ?? "") != "", textView.text != "" { 199 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.4) { 200 | self.showAlert("反馈成功", style: .success) 201 | } 202 | } else { 203 | showAlert("请输入反馈内容", style: .warning) 204 | } 205 | } 206 | 207 | 208 | } 209 | 210 | class TermVC: UIViewController { 211 | 212 | @IBOutlet weak var checker: CheckerButton! 213 | var toHalt = false 214 | 215 | override func viewDidLoad() { 216 | super.viewDidLoad() 217 | checker.setCheckBoxStyle(fontSize: 14) 218 | checker.semanticContentAttribute = .forceLeftToRight 219 | checker.titleEdgeInsets = UIEdgeInsets(top: 0, left: 10, bottom: 0, right: 0) 220 | checker.imageEdgeInsets = UIEdgeInsets(top: 0, left: 5, bottom: 0, right: 0) 221 | if toHalt { 222 | self.showAlert("无网络连接,即将退出程序", style: .failure, duration: 2.0) { 223 | Util.halt() 224 | } 225 | } 226 | } 227 | 228 | override func viewDidAppear(_ animated: Bool) { 229 | super.viewDidAppear(animated) 230 | 231 | if let s = G.blockContent { 232 | self << (UIAlertController(title: "很抱歉,你已被封禁", message: s, preferredStyle: .alert)..{ 233 | $0.addAction(.init(title: "退出", style: .cancel, handler: { _ in 234 | Util.halt() 235 | })) 236 | }) 237 | } 238 | 239 | } 240 | 241 | func noNetwork() -> Self { 242 | toHalt = true 243 | return self 244 | } 245 | 246 | @IBAction func confirm(_ sender: Any) { 247 | if checker.checked { 248 | showAlert("欢迎来到无可奉告", style: .success) { 249 | let vc = *"LoginVC" 250 | vc.modalPresentationStyle = .fullScreen 251 | self << vc 252 | } 253 | } else { 254 | showAlert("请同意用户协议", style: .warning) 255 | } 256 | } 257 | 258 | } 259 | -------------------------------------------------------------------------------- /Forum/OtherViews/NewThreadVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // NewPostViewController.swift 3 | // Forum 4 | // 5 | // Created by Haichen Dong on 2020/10/20. 6 | // 7 | 8 | import UIKit 9 | import DropDown 10 | 11 | class NewThreadVC: UIViewController, UITextFieldDelegate, UITextViewDelegate { 12 | 13 | @IBOutlet weak var titleTextField: UITextField! 14 | @IBOutlet weak var contentTextField: UITextView! 15 | @IBOutlet weak var contentCountLabel: StateLabel! 16 | @IBOutlet weak var titleCountLabel: StateLabel! 17 | @IBOutlet weak var blockBtn: UIButton! 18 | @IBOutlet weak var typeBtn: UIButton! 19 | @IBOutlet weak var checkBtn: CheckerButton! 20 | @IBOutlet weak var tagBtn: UIButton! 21 | 22 | var blocks: [[UIButton]]! 23 | var getResult: (() -> (Int, Int))! 24 | var preFill: (title: String?, content: String?)? 25 | 26 | private var fatherVC: MainVC! 27 | func withFather(_ vc: MainVC) -> Self { 28 | fatherVC = vc 29 | return self 30 | } 31 | 32 | @IBAction func dismissBtnClicked(_ sender: Any) { 33 | dismiss(animated: true) 34 | } 35 | 36 | lazy var blockDropDown = DropDown()..{ 37 | $0.dataSource = Thread.Category.allCases.dropFirst().map { 38 | $0.rawValue 39 | } 40 | $0.backgroundColor = .systemBackground 41 | $0.cellHeight = 40 42 | $0.textColor = self.traitCollection.userInterfaceStyle == .dark ? .lightText : .darkText 43 | } 44 | 45 | lazy var typeDropDown = DropDown()..{ 46 | $0.dataSource = NameTheme.allCases.map { 47 | $0.displayText 48 | } 49 | $0.backgroundColor = .systemBackground 50 | $0.cellHeight = 40 51 | $0.textColor = self.traitCollection.userInterfaceStyle == .dark ? .lightText : .darkText 52 | } 53 | 54 | lazy var tagDropDown = DropDown()..{ 55 | $0.dataSource = ["无需标签"] + Tag.allCases.map {$0.rawValue} 56 | $0.backgroundColor = .systemBackground 57 | $0.cellHeight = 40 58 | $0.textColor = self.traitCollection.userInterfaceStyle == .dark ? .lightText : .darkText 59 | } 60 | 61 | func setTitleContent(_ pre : (title: String?, content: String?)) -> Self { 62 | preFill = pre 63 | return self 64 | } 65 | 66 | override func viewDidLoad() { 67 | super.viewDidLoad() 68 | 69 | // Do any additional setup after loading the view. 70 | let gesture = UITapGestureRecognizer(target: self, action: #selector(self.viewTapped(_:))) 71 | gesture.numberOfTouchesRequired = 1 72 | gesture.cancelsTouchesInView = false 73 | self.view.addGestureRecognizer(gesture) 74 | 75 | contentTextField.delegate = self 76 | contentTextField.placeholder = "请输入内容..." 77 | contentTextField.text = "" 78 | textViewDidChange(contentTextField) 79 | 80 | titleTextField.delegate = self 81 | titleTextField.text = "" 82 | _ = textField(titleTextField, shouldChangeCharactersIn: NSRange(), replacementString: "") 83 | 84 | blockBtn.addTarget(self, action: #selector(chooseBlock(_:)), for: .touchUpInside) 85 | blockBtn.setTitle("请选择分区", for: .normal) 86 | blockBtn.setDropDownStyle(fontSize: 16) 87 | 88 | blockDropDown.anchorView = blockBtn 89 | blockDropDown.selectionAction = { (index: Int, item: String) in 90 | self.blockBtn.setTitle(item, for: .normal) 91 | } 92 | 93 | typeBtn.addTarget(self, action: #selector(chooseBlock(_:)), for: .touchUpInside) 94 | typeBtn.setTitle(typeDropDown.dataSource.first!, for: .normal) 95 | typeBtn.setDropDownStyle(fontSize: 16) 96 | 97 | typeDropDown.anchorView = typeBtn 98 | typeDropDown.selectionAction = { (index: Int, item: String) in 99 | self.typeBtn.setTitle(item, for: .normal) 100 | } 101 | typeDropDown.selectRow(0) 102 | 103 | tagBtn.addTarget(self, action: #selector(chooseBlock(_:)), for: .touchUpInside) 104 | tagBtn.setTitle("请选择标签", for: .normal) 105 | tagBtn.setDropDownStyle(fontSize: 16) 106 | 107 | tagDropDown.anchorView = tagBtn 108 | tagDropDown.selectionAction = { (index: Int, item: String) in 109 | self.tagBtn.setTitle(item, for: .normal) 110 | } 111 | 112 | checkBtn.setCheckBoxStyle(fontSize: 14) 113 | checkBtn.setTitle("人名随机排序", for: .normal) 114 | 115 | addKeyCommand(.init(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(esc))) 116 | 117 | if let t = preFill?.title { 118 | titleTextField.text = t 119 | } 120 | if let c = preFill?.content { 121 | contentTextField.text = c 122 | } 123 | } 124 | 125 | @objc func chooseBlock(_ sender: UIButton) { 126 | if sender === blockBtn { 127 | blockDropDown.show() 128 | } else if sender === tagBtn { 129 | tagDropDown.show() 130 | } else { 131 | typeDropDown.show() 132 | } 133 | } 134 | 135 | func textViewDidChange(_ textView: UITextView) { 136 | updateCountingLabel(label: contentCountLabel, text: contentTextField.text, lineLimit: 20, charLimit: 817) 137 | } 138 | 139 | func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool { 140 | updateCountingLabel(label: titleCountLabel, text: titleTextField.text ?? "", lineLimit: 1, charLimit: 40) 141 | return true 142 | } 143 | 144 | func textFieldDidEndEditing(_ textField: UITextField) { 145 | updateCountingLabel(label: titleCountLabel, text: titleTextField.text ?? "", lineLimit: 1, charLimit: 40) 146 | } 147 | 148 | @objc func viewTapped(_ sender: Any) { 149 | self.view.endEditing(false) 150 | } 151 | 152 | @IBAction func postBtnClicked(_ sender: Any) { 153 | if let postTitle = titleTextField.text, postTitle != "", let postContent = contentTextField.text, postContent != "" { 154 | if titleCountLabel.ok && contentCountLabel.ok { 155 | if let block = Thread.Category(rawValue: blockDropDown.selectedItem ?? "") { 156 | if let selTag = tagDropDown.selectedItem { 157 | (showProgress(), self.typeDropDown.selectedItem!)..{ bar, theme in 158 | DispatchQueue.global().async { 159 | if Network.newThread( 160 | title: postTitle, inBlock: block, content: postContent, 161 | anonymousType: NameTheme.allCases.first(where: {$0.displayText == theme})! , 162 | seed: self.checkBtn.checked ? Int.random(in: 1..<1000000) : 0, tag: Tag(rawValue: selTag) 163 | ) { 164 | self.setAndHideAlert(bar, "发帖成功", style: .success) { 165 | self.fatherVC.refresh() 166 | self.dismiss(animated: true) 167 | } 168 | } else { self.setAndHideAlert(bar, "发帖失败", style: .failure) } 169 | } 170 | } 171 | } else { showAlert("请选择标签", style: .warning) } 172 | } else { showAlert("请选择一个分区", style: .warning) } 173 | } else { showAlert("请输入合适长度的内容", style: .warning) } 174 | } else { showAlert("请输入合适长度的内容", style: .warning) } 175 | } 176 | 177 | override func viewDidLayoutSubviews() { 178 | super.viewDidLayoutSubviews() 179 | } 180 | 181 | /* 182 | // MARK: - Navigation 183 | 184 | // In a storyboard-based application, you will often want to do a little preparation before navigation 185 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 186 | // Get the new view controller using segue.destination. 187 | // Pass the selected object to the new view controller. 188 | } 189 | */ 190 | 191 | } 192 | -------------------------------------------------------------------------------- /Forum/SceneDelegate.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SceneDelegate.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/9/20. 6 | // 7 | 8 | import UIKit 9 | 10 | @available(iOS 13.0, *) 11 | class SceneDelegate: UIResponder, UIWindowSceneDelegate { 12 | 13 | var window: UIWindow? 14 | 15 | 16 | func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) { 17 | // Use this method to optionally configure and attach the UIWindow `window` to the provided UIWindowScene `scene`. 18 | // If using a storyboard, the `window` property will automatically be initialized and attached to the scene. 19 | // This delegate does not imply the connecting scene or session are new (see `application:configurationForConnectingSceneSession` instead). 20 | guard let _ = (scene as? UIWindowScene) else { return } 21 | 22 | if let urlContext = connectionOptions.urlContexts.first { 23 | dealWithURLContext(urlContext) 24 | } 25 | 26 | // DispatchQueue.global().async { 27 | // if let res = isUpdateAvailable(), res.0 { 28 | // G.updateAvailable = (res.1, res.2) 29 | // } 30 | // } 31 | 32 | var pr = G.viewStyle.content, npr = [String: Int]() 33 | for cs in Tag.allCases.map({String(describing: $0)}) { 34 | npr[cs] = pr[cs] ?? 0 35 | } 36 | G.viewStyle.content = npr 37 | 38 | if let (success, blockString) = Network.verifyToken() { 39 | if success != "1" { 40 | window?.rootViewController = *"TermVC" 41 | if success == "-1" { 42 | G.blockContent = blockString 43 | } 44 | } else { 45 | window?.rootViewController = *"InitTabVC" 46 | } 47 | } else { 48 | window?.rootViewController = (*"TermVC" as! TermVC).noNetwork() 49 | } 50 | 51 | } 52 | 53 | func scene(_ scene: UIScene, openURLContexts URLContexts: Set) { 54 | if let urlContext = URLContexts.first { 55 | if let mvc = window?.rootViewController?.topMostViewController() { 56 | if mvc is LoginVC || mvc is TermVC { 57 | mvc.showAlert("请先登录", style: .failure, duration: 2) 58 | } else { 59 | dealWithURLContext(urlContext) 60 | if let id = G.openThreadID { 61 | G.openThreadID = nil 62 | Thread.Manager.openCertainThread(mvc, id: id) 63 | } else if let nt = G.openNewThread { 64 | G.openNewThread = nil 65 | mvc << (*"NewThreadVC" as! NewThreadVC).setTitleContent(nt) 66 | } else { 67 | mvc.showAlert("链接格式错误", style: .warning, duration: 1.5) 68 | } 69 | } 70 | } 71 | } 72 | 73 | } 74 | 75 | func sceneDidDisconnect(_ scene: UIScene) { 76 | // Called as the scene is being released by the system. 77 | // This occurs shortly after the scene enters the background, or when its session is discarded. 78 | // Release any resources associated with this scene that can be re-created the next time the scene connects. 79 | // The scene may re-connect later, as its session was not necessarily discarded (see `application:didDiscardSceneSessions` instead). 80 | } 81 | 82 | func sceneDidBecomeActive(_ scene: UIScene) { 83 | // Called when the scene has moved from an inactive state to an active state. 84 | // Use this method to restart any tasks that were paused (or not yet started) when the scene was inactive. 85 | } 86 | 87 | func sceneWillResignActive(_ scene: UIScene) { 88 | // Called when the scene will move from an active state to an inactive state. 89 | // This may occur due to temporary interruptions (ex. an incoming phone call). 90 | } 91 | 92 | func sceneWillEnterForeground(_ scene: UIScene) { 93 | // Called as the scene transitions from the background to the foreground. 94 | // Use this method to undo the changes made on entering the background. 95 | } 96 | 97 | func sceneDidEnterBackground(_ scene: UIScene) { 98 | // Called as the scene transitions from the foreground to the background. 99 | // Use this method to save data, release shared resources, and store enough scene-specific state information 100 | // to restore the scene back to its current state. 101 | 102 | // Save changes in the application's managed object context when the application transitions to the background. 103 | (UIApplication.shared.delegate as? AppDelegate)?.saveContext() 104 | } 105 | 106 | 107 | } 108 | -------------------------------------------------------------------------------- /Forum/TableViews/MainCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainCell.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/9/29. 6 | // 7 | 8 | import UIKit 9 | import DropDown 10 | 11 | class MainCell: UITableViewCell, UITextViewDelegate { 12 | 13 | enum Scene { 14 | case thread, floor, message 15 | } 16 | 17 | var scene = Scene.thread 18 | 19 | var thread: Thread! 20 | var floor: Floor! 21 | var message: Message! 22 | var isFirstFloor = false 23 | var parentVC: MainVC! 24 | var name: String = "_" 25 | 26 | @IBOutlet weak var mainView: UIView! 27 | @IBOutlet weak var idLabel: UILabel! 28 | @IBOutlet weak var idLabelHeight: NSLayoutConstraint! 29 | @IBOutlet weak var timeLabel: UILabel! 30 | @IBOutlet weak var headLabel: UILabel! 31 | @IBOutlet weak var headWidth: NSLayoutConstraint! 32 | @IBOutlet weak var likedBtn: UIButton! 33 | @IBOutlet weak var commentBtn: UIButton! 34 | @IBOutlet weak var readBtn: UIButton! 35 | @IBOutlet weak var cornerLabel: UILabel! 36 | @IBOutlet weak var titleLabel: UILabel! 37 | @IBOutlet weak var titleDist: NSLayoutConstraint! 38 | @IBOutlet weak var commentDist: NSLayoutConstraint! 39 | @IBOutlet weak var idHeight: NSLayoutConstraint! 40 | @IBOutlet weak var headImageView: UIImageView! 41 | @IBOutlet weak var mainTextView: UITextView! 42 | @IBOutlet weak var replyToName: UIButton! 43 | @IBOutlet weak var replyToNameDist: NSLayoutConstraint! 44 | @IBOutlet weak var cornerWidth: NSLayoutConstraint! 45 | @IBOutlet weak var cornerHeight: NSLayoutConstraint! 46 | @IBOutlet weak var orderBtn: UIButton! 47 | @IBOutlet weak var footerHeight: NSLayoutConstraint! 48 | @IBOutlet weak var higherTitleLabel: UILabel! 49 | @IBOutlet weak var higherTitleDist: NSLayoutConstraint! 50 | @IBOutlet weak var allTopDIst: NSLayoutConstraint! 51 | @IBOutlet weak var lessThanDIst: NSLayoutConstraint! 52 | @IBOutlet weak var higherTitleLeadingDist: NSLayoutConstraint! 53 | @IBOutlet weak var titleLeadingDist: NSLayoutConstraint! 54 | @IBOutlet weak var blockBottomDist: NSLayoutConstraint! 55 | 56 | var content = (title: "", content: "") { 57 | didSet { 58 | for v in higherTitleLabel.subviews {v.removeFromSuperview()} 59 | if let t = thread.tag?.rawValue, scene == .thread || isFirstFloor { 60 | let w = (t as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]).width + 10 61 | higherTitleLeadingDist.constant = w + 16 62 | higherTitleLabel += UILabel(frame: .init(x: -w-8, y: 0, width: w, height: 21)).setAsTagLabel(t) 63 | higherTitleLabel.clipsToBounds = false 64 | higherTitleLabel.layer.masksToBounds = false 65 | } else { 66 | higherTitleLeadingDist.constant = 8 67 | } 68 | titleLabel.text = content.title 69 | higherTitleLabel.text = content.title 70 | 71 | mainTextView.text = content.content 72 | 73 | if scene != .thread { 74 | titleLabel.isHidden = true 75 | titleDist.constant = -titleLabel.frame.height 76 | } 77 | if !isFirstFloor { 78 | higherTitleLabel.isHidden = true 79 | allTopDIst.constant = 0 80 | lessThanDIst.constant = 0 81 | } else { 82 | higherTitleLabel.isHidden = false 83 | allTopDIst.constant = 8 84 | lessThanDIst.constant = 100 85 | } 86 | 87 | } 88 | } 89 | 90 | var folded: Bool = false { 91 | didSet { 92 | (idLabel.subviews + cornerLabel.subviews).forEach({$0.removeFromSuperview()}) 93 | if folded || (scene == .thread && thread.tag != nil) { 94 | let t = (scene == .thread ? thread.tag?.rawValue : nil) ?? "被踩过多" 95 | let w = (t as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 15)]).width + 10 96 | (floor != nil ? cornerLabel : idLabel) += UILabel(frame: .init(x: floor != nil ? -10 : 65, y: 5, width: w, height: 20)).setAsTagLabel(t) 97 | mainView.bringSubviewToFront(floor != nil ? cornerLabel : idLabel) 98 | } 99 | if folded { 100 | blockBottomDist.constant = 6 101 | replyToName.isEnabled = false 102 | } else { 103 | blockBottomDist.constant = 3000 104 | replyToName.isEnabled = true 105 | } 106 | } 107 | } 108 | 109 | override func awakeFromNib() { 110 | super.awakeFromNib() 111 | 112 | // Initialization code 113 | selectionStyle = .none 114 | likedBtn.adjustsImageWhenHighlighted = true 115 | likedBtn.adjustsImageWhenDisabled = false 116 | commentBtn.adjustsImageWhenDisabled = false 117 | readBtn.adjustsImageWhenDisabled = false 118 | timeLabel.textColor = .gray 119 | timeLabel.text = "" 120 | replyToName.setTitle("", for: .normal) 121 | replyToName.isEnabled = false 122 | contentView.clipsToBounds = false 123 | contentView.superview?.clipsToBounds = false 124 | 125 | mainTextView.translatesAutoresizingMaskIntoConstraints = false 126 | mainTextView.sizeToFit() 127 | mainTextView.isScrollEnabled = false 128 | mainTextView.contentInset = .zero 129 | mainTextView.textContainer.lineFragmentPadding = 0 130 | mainTextView.isEditable = false 131 | mainTextView.isSelectable = false 132 | mainTextView.isUserInteractionEnabled = false 133 | mainTextView.delegate = self 134 | mainTextView.backgroundColor = .tertiarySystemBackground 135 | 136 | footerHeight.constant = 0 137 | orderBtn.isHidden = true 138 | orderBtn.addTarget(self, action: #selector(orderBtnClicked(_:)), for: .touchUpInside) 139 | } 140 | 141 | override func setSelected(_ selected: Bool, animated: Bool) { 142 | super.setSelected(selected, animated: animated) 143 | 144 | // Configure the view for the selected state 145 | } 146 | 147 | func textView(_ textView: UITextView, shouldInteractWith URL: URL, in characterRange: NSRange, interaction: UITextItemInteraction) -> Bool { 148 | print(URL, "\(interaction.rawValue)") 149 | if (URL.absoluteString.hasPrefix("http://wukefenggao.cn/viewThread/") || URL.absoluteString.hasPrefix("wukefenggao.cn/viewThread/")) && interaction == .invokeDefaultAction { 150 | let u = UIKit.URL(string: "wkfg://" + URL.absoluteString.replacingOccurrences(of: "http://wukefenggao.cn/viewThread/", with: "").replacingOccurrences(of: "wukefenggao.cn/viewThread/", with: ""))! 151 | UIApplication.shared.open(u) 152 | return false 153 | } else { 154 | return true 155 | } 156 | } 157 | 158 | var liked: LikeState = .none { 159 | didSet { 160 | likedBtn.setImage( 161 | UIImage(systemName: liked.rawValue, 162 | withConfiguration: UIImage.SymbolConfiguration(scale: .small)), 163 | for: .normal 164 | ) 165 | } 166 | } 167 | 168 | @IBAction func like(_ sender: Any) { 169 | self.likedBtn.isEnabled = false 170 | DispatchQueue.global().async { 171 | {() -> Bool in 172 | switch self.liked { 173 | case .none: 174 | if sender is Int { 175 | return self.floor.id == "0" 176 | ? Network.dislikeThread(for: self.thread.id) 177 | : Network.dislikeFloor(for: self.thread.id, floor: self.floor.id) 178 | } else { 179 | return self.floor.id == "0" 180 | ? Network.likeThread(for: self.thread.id) 181 | : Network.likeFloor(for: self.thread.id, floor: self.floor.id) 182 | } 183 | case .like: 184 | return self.floor.id == "0" 185 | ? Network.cancelLikeThread(for: self.thread.id) 186 | : Network.cancelLikeFloor(for: self.thread.id, floor: self.floor.id) 187 | case .disL: 188 | return self.floor.id == "0" 189 | ? Network.cancelDislikeThread(for: self.thread.id) 190 | : Network.canceldislikeFloor(for: self.thread.id, floor: self.floor.id) 191 | } 192 | }()..{ success in 193 | DispatchQueue.main.async { 194 | self.likedBtn.isEnabled = true 195 | if success { 196 | switch self.liked { 197 | case .none: 198 | if sender is Int { 199 | self.liked = .disL; self.floor.nDisliked += 1 200 | } else { 201 | self.liked = .like; self.floor.nLiked += 1 202 | } 203 | case .like: self.liked = .none; self.floor.nLiked -= 1 204 | case .disL: self.liked = .none; self.floor.nLiked += 1 205 | } 206 | if let dd = self.parentVC.d as? Floor.Manager, let i = dd.data.firstIndex(where: {$0.id == self.floor.id}) { 207 | dd.data[i].setLikeStatus(nLiked: self.floor.nLiked, nDisliked: self.floor.nDisliked, hasLiked: self.liked) 208 | } 209 | self.likedBtn.setTitle("\(self.floor.nLiked - self.floor.nDisliked)", for: .normal) 210 | } else { self.parentVC.showAlert("网络错误", style: .failure) } 211 | } 212 | } 213 | } 214 | } 215 | 216 | func withVC(_ vc: MainVC) -> Self { 217 | parentVC = vc 218 | return self 219 | } 220 | 221 | @IBAction func comment(_ sender: Any) { 222 | parentVC.tryToReplyTo(floor: floor.id) 223 | } 224 | 225 | // MARK: - Thread 226 | 227 | func setAs(thread t: Thread, allowFold: Bool = true) -> Self { 228 | thread = t 229 | scene = .thread 230 | 231 | idLabel.text = "#\(t.id)" 232 | 233 | var disp = "", limit = 4 234 | _ = t.content.components(separatedBy: "\n").reduce(0) { 235 | if $0 == limit { 236 | disp += "..." 237 | } else if $0 < limit { 238 | disp += ($0 == 0 ? "" : "\n") + $1 239 | } 240 | return $0 + Int(($1 as NSString).boundingRect(with: .init(width: mainView.frame.width - 16, height: CGFloat.greatestFiniteMagnitude), options: .usesLineFragmentOrigin, attributes: [NSAttributedString.Key.font: mainTextView.font!], context: nil).height / mainTextView.font!.lineHeight) 241 | } 242 | mainTextView.textContainer.maximumNumberOfLines = limit 243 | mainTextView.textContainer.lineBreakMode = .byTruncatingTail 244 | content = (t.title, disp) 245 | 246 | likedBtn.setTitle("\(t.nLiked - t.nDislike)", for: .normal) 247 | readBtn.setTitle("\(t.nRead)", for: .normal) 248 | commentBtn.setTitle("\(t.nCommented)", for: .normal) 249 | 250 | cornerWidth.constant = 60 251 | cornerLabel.text = Util.dateToDeltaString(t.lastUpdateTime) 252 | 253 | if thread.isTop { 254 | headLabel.text = String("公告") 255 | headLabel.layer.backgroundColor = t.color[0].cgColor 256 | headLabel.layer.cornerRadius = 15 257 | headLabel.textColor = .white 258 | headLabel.font = .systemFont(ofSize: 12, weight: .medium) 259 | headImageView.image = UIImage() 260 | headImageView.backgroundColor = nil 261 | } else { 262 | headLabel.text = "" 263 | headLabel.layer.backgroundColor = .none 264 | headImageView.backgroundColor = t.color[0] 265 | headImageView.image = UIImage(named: "hatw80") 266 | headImageView.layer.cornerRadius = headImageView.frame.height / 2 267 | } 268 | idLabelHeight.constant = idHeight.constant 269 | 270 | likedBtn.isEnabled = false 271 | commentBtn.isEnabled = false 272 | readBtn.isEnabled = false 273 | 274 | folded = thread.folded && allowFold 275 | 276 | return self 277 | } 278 | 279 | // MARK: - Floor 280 | 281 | @objc func moveTo(_ sender: Any) { 282 | (parentVC.d as! Floor.Manager, String(floor.replyToFloor!))..{ (dd, to) in 283 | if floor.replyToFloor! > 0, let idx = dd.data.firstIndex(where: {$0.id == to}) { 284 | parentVC.tableView.scrollToRow(at: .init(row: idx + 1, section: 0), at: dd.order == .earliest ? .top : .none, animated: true) 285 | } else { 286 | parentVC.showAlert("该楼层暂未加载或不存在", style: .warning) 287 | } 288 | } 289 | } 290 | 291 | func setAs(floor f: Floor, forThread t: Thread, firstFloor: Bool = false, order o: Order = .earliest) -> Self { 292 | if f.fake { 293 | return self 294 | } 295 | thread = t 296 | floor = f 297 | isFirstFloor = firstFloor 298 | scene = .floor 299 | order = o 300 | self.orderBtn.setTitle(order.rawValue, for: .normal) 301 | 302 | name = t.name[Int(f.name)!] 303 | 304 | idLabel.text = name + 305 | (((f.replyToFloor ?? 0) == 0) 306 | ? "" 307 | : " 回复 " 308 | ) 309 | if (f.replyToFloor ?? 0) != 0 { 310 | replyToName.isEnabled = true 311 | replyToNameDist.constant = (idLabel.text! as NSString).size(withAttributes: [NSAttributedString.Key.font: UIFont.systemFont(ofSize: 13)]).width + 8 312 | replyToName.setTitle("#\(f.replyToFloor!) \(t.name[Int(f.replyToName!)!])", for: .normal) 313 | replyToName.setTitleColor(t.color[Int(f.replyToName!)!], for: .normal) 314 | replyToName.addTarget(self, action: #selector(moveTo(_:)), for: .touchUpInside) 315 | } else { 316 | replyToName.isEnabled = false 317 | replyToName.setTitle("", for: .normal) 318 | replyToName.removeTarget(self, action: #selector(moveTo(_:)), for: .touchUpInside) 319 | } 320 | timeLabel.text = Util.dateToDeltaString(f.time) 321 | 322 | headLabel.text = String(name.components(separatedBy: " ").last!.first!) 323 | headLabel.layer.backgroundColor = t.color[Int(f.name)!].cgColor 324 | headLabel.layer.cornerRadius = 15 325 | headLabel.textColor = .white 326 | headLabel.font = .systemFont(ofSize: 20, weight: .medium) 327 | 328 | content = (isFirstFloor ? t.title : "", f.content) 329 | likedBtn.setTitle("\(f.nLiked - f.nDisliked)", for: .normal) 330 | cornerLabel.text = "#\(floor.id)" 331 | cornerLabel.fontSize = 14 332 | 333 | liked = f.hasLiked 334 | commentBtn.setTitle("回复", for: .normal) 335 | commentBtn.contentHorizontalAlignment = .right 336 | 337 | commentDist.constant = -readBtn.frame.width 338 | readBtn.isHidden = true 339 | mainTextView.isUserInteractionEnabled = true 340 | mainTextView.isSelectable = true 341 | 342 | 343 | likedBtn.adjustsImageWhenDisabled = true 344 | likedBtn.isHidden = isFirstFloor && !t.isFromFloorList 345 | orderBtn.isHidden = !isFirstFloor 346 | orderBtn.isEnabled = true 347 | footerHeight.constant = isFirstFloor ? 35 : 0 348 | 349 | folded = floor.folded && !isFirstFloor 350 | 351 | return self 352 | } 353 | 354 | var order: Order = .earliest 355 | 356 | 357 | @objc func orderBtnClicked(_ sender: UIButton) { 358 | DropDown(anchorView: sender, selectionAction: { (i, s) in 359 | if self.order.rawValue != s { 360 | self.order = Order.allCases.first{$0.rawValue == s}! 361 | self.orderBtn.setTitle(self.order.rawValue, for: .normal) 362 | self.orderBtn.isEnabled = false 363 | self.parentVC.setReplyOrder(self.order) 364 | } 365 | }, dataSource: Order.allCases.map{$0.rawValue}).show() 366 | } 367 | 368 | // MARK: - Message 369 | 370 | func setAs(message m: Message) -> Self { 371 | message = m 372 | _ = setAs(thread: m.thread, allowFold: false) 373 | cornerLabel.text = m.judge == 0 ? "未读" : "已读" 374 | cornerLabel.textColor = m.judge == 0 ? .systemBlue : .label 375 | content.content = m.ty == 0 ? "有人回复了你!" : "有\(m.ty)人赞了你!" 376 | return self 377 | } 378 | 379 | override func layoutSubviews() { 380 | super.layoutSubviews() 381 | contentView.superview?.layer.masksToBounds = false 382 | mainView.applyCardStyle(clip: folded) 383 | } 384 | 385 | } 386 | -------------------------------------------------------------------------------- /Forum/TableViews/MainCell.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 35 | 51 | 67 | 76 | 85 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 119 | 128 | 133 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 | 232 | 233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | 261 | 262 | 263 | 264 | 265 | 266 | 267 | 268 | 269 | 270 | 271 | 272 | -------------------------------------------------------------------------------- /Forum/TableViews/MainVC.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainTableViewController.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/9/20. 6 | // 7 | 8 | import UIKit 9 | import MJRefresh 10 | import UITextView_Placeholder 11 | import DropDown 12 | 13 | extension UIRefreshControl { 14 | func refreshManually() { 15 | if let scrollView = superview as? UIScrollView { 16 | scrollView.setContentOffset(CGPoint(x: 0, y: scrollView.contentOffset.y - frame.height), animated: false) 17 | } 18 | beginRefreshing() 19 | sendActions(for: .valueChanged) 20 | } 21 | } 22 | 23 | extension UIViewController { 24 | @objc func esc() { 25 | self.dismiss(animated: true, completion: nil) 26 | } 27 | } 28 | 29 | class MainVC: UIViewController, UITableViewDelegate, UITableViewDataSource, DoubleTappable { 30 | 31 | enum Scene: String { 32 | case main = "首页", my = "我的帖子", trends = "趋势", messages = "通知", floors = "Thread#", favour = "我的收藏" 33 | } 34 | 35 | var scene = Scene.main 36 | var d: BaseManager = Thread.Manager(type: .time) 37 | 38 | @IBOutlet weak var tableView: UITableView! 39 | @IBOutlet weak var textView: UITextView! 40 | @IBOutlet weak var bottomSpace: NSLayoutConstraint! 41 | @IBOutlet weak var bottomViewHeight: NSLayoutConstraint! 42 | @IBOutlet weak var textViewHeight: NSLayoutConstraint! 43 | @IBOutlet weak var replyCountLabel: StateLabel! 44 | @IBOutlet weak var replyToLabel: UILabel! 45 | @IBOutlet weak var newThreadBtn: UIButton! 46 | // @IBOutlet weak var barFirstBtn: UIBarButtonItem! 47 | // @IBOutlet weak var barSecondBtn: UIBarButtonItem! 48 | // @IBOutlet weak var barThirdBtn: UIBarButtonItem! 49 | @IBOutlet weak var topDist: NSLayoutConstraint! 50 | 51 | var inSearchMode = false 52 | 53 | static func new(_ scene: Scene, _ args: Any...) -> MainVC { 54 | let vc = *"MainVC" as! MainVC 55 | vc.scene = scene 56 | switch vc.scene { 57 | case .my: 58 | vc.d = Thread.Manager(type: .my) 59 | case .trends: 60 | vc.d = Thread.Manager(type: .trending) 61 | case .favour: 62 | vc.d = Thread.Manager(type: .favoured) 63 | case .floors: 64 | vc.d = Floor.Manager(args[0] as! Thread) 65 | case .messages: 66 | vc.d = Message.Manager() 67 | case .main: 68 | fatalError() 69 | } 70 | return vc 71 | } 72 | 73 | var inPreview = false 74 | var isDoubleTapping = false, tryDoubleTapping = false, firstLoading = true 75 | var _topDist: CGFloat { 76 | inPreview 77 | ? 0 78 | : (UIApplication.shared.windows[0].safeAreaInsets.top == 20 ? 64 : 88) 79 | } 80 | var isRefreshing = false 81 | 82 | lazy var refreshControl = UIRefreshControl()..{ 83 | $0.addTarget(self, action: #selector(refresh), for: .valueChanged) 84 | } 85 | lazy var dropDown = DropDown()..{ 86 | $0.dataSource = Thread.Category.allCases.map { 87 | $0.rawValue 88 | } 89 | $0.backgroundColor = .systemBackground 90 | $0.cellHeight = 40 91 | $0.textColor = self.traitCollection.userInterfaceStyle == .dark ? .lightText : .darkText 92 | } 93 | lazy var search = SearchBarContainerView(customSearchBar: UISearchBar())..{ 94 | $0.frame = CGRect(x: 0, y: 0, width: self.view.frame.width, height: 44) 95 | $0.searchBar.delegate = self 96 | } 97 | lazy var footer = MJRefreshAutoNormalFooter()..{ 98 | $0.setRefreshingTarget(self, refreshingAction: #selector(loadmore)) 99 | $0.heightPreset = .small 100 | $0.backgroundColor = .none 101 | $0.setTitle(scene == .floors ? "" : "正在加载...", for: .idle) 102 | } 103 | 104 | var floor: String = "0" { 105 | didSet { 106 | self.replyToLabel.text = "To #\(self.floor) \((self.d as! Floor.Manager).displayNameFor(Int(self.floor)!)):" 107 | } 108 | } 109 | 110 | override func viewDidLoad() { 111 | super.viewDidLoad() 112 | 113 | topDist.constant = _topDist 114 | 115 | UINavigationBarAppearance()..{ 116 | $0.configureWithOpaqueBackground() 117 | $0.shadowImage = nil 118 | $0.shadowColor = nil 119 | $0.backgroundColor = .systemBackground 120 | navigationController?.navigationBar.standardAppearance = $0 121 | navigationController?.navigationBar.scrollEdgeAppearance = $0 122 | } 123 | 124 | self.extendedLayoutIncludesOpaqueBars = true 125 | 126 | tableView!..{ 127 | $0.contentInsetAdjustmentBehavior = .always 128 | $0.delegate = self 129 | $0.dataSource = self 130 | $0.register(UINib(nibName: "MainCell", bundle: .main), forCellReuseIdentifier: "MainCell") 131 | $0.separatorStyle = .none 132 | 133 | $0.mj_footer = footer 134 | !inPreview => $0.refreshControl = refreshControl 135 | 136 | $0.addGestureRecognizer(UITapGestureRecognizer(target: self, action: #selector(self.viewTapped(_:)))..{ 137 | $0.numberOfTouchesRequired = 1 138 | $0.cancelsTouchesInView = false 139 | }) 140 | $0.isScrollEnabled = false 141 | } 142 | 143 | addKeyCommand(.init(input: UIKeyCommand.inputEscape, modifierFlags: [], action: #selector(esc))) 144 | NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillShow(_:)), name: UIResponder.keyboardWillShowNotification, object: nil) 145 | NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardWillHide(_:)), name: UIResponder.keyboardWillHideNotification, object: nil) 146 | NotificationCenter.default.addObserver(self, selector: #selector(self.keyboardDidHide(_:)), name: UIResponder.keyboardDidHideNotification, object: nil) 147 | 148 | textView.delegate = self 149 | textViewDidChange(textView) 150 | replyToLabel.text = "To #0" 151 | refresh() 152 | } 153 | 154 | override func viewWillAppear(_ animated: Bool) { 155 | newThreadBtn.applyShadow(opaque: false, offset: 2, opacity: 0.3) 156 | navigationItem.largeTitleDisplayMode = scene == .floors ? .never : .always 157 | navigationItem.title = scene == .floors 158 | ? "\((d as! Floor.Manager).thread.title)" 159 | : scene.rawValue 160 | 161 | if scene == .floors { 162 | self.tabBarController?.tabBar.isHidden = true 163 | self.bottomViewHeight.constant = self.textViewHeight.constant + 16 + 10 + 20 164 | !firstLoading => updateFavour() 165 | } else { 166 | self.tabBarController?.tabBar.isHidden = false 167 | self.bottomViewHeight.constant = 0 168 | } 169 | if scene == .main { 170 | newThreadBtn.frame.origin = CGPoint(x: newThreadBtn.frame.minX, y: UIScreen.main.bounds.height - tabBarController!.tabBar.frame.height - 80) 171 | navigationController?.navigationBar.layer.shadowOpacity = 0 172 | } else { 173 | newThreadBtn.isHidden = true 174 | navigationController?.navigationBar.applyShadow() 175 | } 176 | 177 | if scene == .main { 178 | navigationItem.rightBarButtonItems = [ 179 | .imgItem(UIImage(systemName: "magnifyingglass", withConfiguration: UIImage.SymbolConfiguration(scale: .large)), action: #selector(firstBtnClicked(_:)), to: self) 180 | ] 181 | } else if scene == .floors { 182 | navigationItem.rightBarButtonItems = [ 183 | .imgItem(UIImage(systemName: "ellipsis.circle", withConfiguration: UIImage.SymbolConfiguration(scale: .large)), action: #selector(firstBtnClicked(_:)), to: self), 184 | .imgItem(UIImage(systemName: "square.and.arrow.up", withConfiguration: UIImage.SymbolConfiguration(scale: .large)), action: #selector(secondBtnClicked(_:)), to: self), 185 | .imgItem(UIImage(systemName: "star", withConfiguration: UIImage.SymbolConfiguration(scale: .large)), action: #selector(thirdBtnClicked(_:)), to: self)..{$0.isEnabled = false} 186 | ] 187 | print("view will appear!", navigationItem.rightBarButtonItems![2].isEnabled) 188 | } 189 | 190 | if let id = G.openThreadID { 191 | G.openThreadID = nil 192 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 193 | Thread.Manager.openCertainThread(self, id: id) 194 | } 195 | } 196 | if let nt = G.openNewThread { 197 | G.openNewThread = nil 198 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { 199 | self << (*"NewThreadVC" as! NewThreadVC).setTitleContent(nt) 200 | } 201 | } 202 | 203 | if let dd = d as? Thread.Manager { 204 | let pp = G.viewStyle.content 205 | for cs in Tag.allCases.map({String(describing: $0)}) { 206 | if (dd.pr[cs] ?? 0) != (pp[cs] ?? 0) { 207 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { 208 | self.hasTappedAgain() 209 | } 210 | break 211 | } 212 | } 213 | } 214 | 215 | // DispatchQueue.global().async { 216 | // while true { 217 | // sleep(1) 218 | // if let up = G.updateAvailable { 219 | // DispatchQueue.main.async { 220 | // self << (UIAlertController(title: "有更新可用", message: "您当前版本为: \(up.1)\n最新版本为: \(up.0)", preferredStyle: .alert)..{ 221 | // $0.addAction(.init(title: "取消", style: .cancel, handler: { _ in 222 | // G.dismissedUpdate = dis 223 | // })) 224 | // }) 225 | // G.updateAvailable = nil 226 | // } 227 | // } 228 | // } 229 | // } 230 | } 231 | 232 | override func viewWillDisappear(_ animated: Bool) { 233 | super.viewWillDisappear(animated) 234 | changeBar(hide: false) 235 | } 236 | 237 | func tryToReplyTo(floor f: String) { 238 | floor = f 239 | textView.becomeFirstResponder() 240 | self.textView.text = "" 241 | self.textViewDidChange(self.textView) 242 | } 243 | 244 | // MARK: - IBActions 245 | 246 | @IBAction func newComment(_ sender: Any) { 247 | if let content = textView.text, content != "", replyCountLabel.ok { 248 | let threadID = (d as! Floor.Manager).thread.id 249 | self.view.endEditing(false) 250 | self.showProgress()..{ bar in 251 | DispatchQueue.global().async { 252 | // print("start to reply...", threadID, self.) 253 | if Network.newReply(for: threadID, floor: self.floor, content: content) { 254 | self.setAndHideAlert(bar, "评论成功", style: .success) { 255 | self.textView.text = "" 256 | self.textViewDidChange(self.textView) 257 | (self.d as! Floor.Manager)..{ 258 | $0.count > 1 && $0.order == .earliest 259 | ?> self.loadmore() 260 | ?< self.refresh() 261 | } 262 | } 263 | } else { self.setAndHideAlert(bar, "评论失败", style: .failure) } 264 | } 265 | } 266 | } else { showAlert("请输入合适长度的内容", style: .warning) } 267 | } 268 | 269 | @IBAction func newThread(_ sender: Any) { 270 | self << (*"NewThreadVC" as! NewThreadVC).withFather(self) 271 | } 272 | 273 | @objc func secondBtnClicked(_ sender: UIButton) { 274 | print(type(of: sender)) 275 | scene == .floors => { 276 | self << (UIActivityViewController(activityItems: [URL(string: "http://wukefenggao.cn/viewThread/\((d as! Floor.Manager).thread.id)")!], applicationActivities: nil)..{ 277 | $0.popoverPresentationController?.sourceView = sender 278 | }) 279 | } 280 | } 281 | 282 | @objc func thirdBtnClicked(_ sender: UIBarButtonItem) { 283 | scene == .floors => { 284 | sender.isEnabled = false 285 | (d as! Floor.Manager)..{ dd in 286 | DispatchQueue.global().async { 287 | let success = dd.thread.hasFavoured 288 | ? Network.cancelFavourThread(for: dd.thread.id) 289 | : Network.favourThread(for: dd.thread.id) 290 | DispatchQueue.main.async { 291 | sender.isEnabled = true 292 | if success { 293 | dd.thread.hasFavoured = !dd.thread.hasFavoured 294 | self.updateFavour() 295 | } else { self.showAlert("收藏失败", style: .failure) } 296 | } 297 | } 298 | } 299 | } 300 | } 301 | 302 | // MARK: - Table view data source 303 | 304 | func numberOfSections(in tableView: UITableView) -> Int { 305 | 1 306 | } 307 | 308 | var headerHeight: CGFloat { 309 | scene == .main ? 20 : 5 310 | } 311 | 312 | func tableView(_ tableView: UITableView, heightForHeaderInSection section: Int) -> CGFloat { 313 | headerHeight 314 | } 315 | 316 | @objc func chooseBlock(_ sender: Any) { 317 | dropDown.show() 318 | } 319 | 320 | func tableView(_ tableView: UITableView, viewForHeaderInSection section: Int) -> UIView? { 321 | let width = tableView.frame.width 322 | return scene != .main 323 | ? UIView() 324 | : UIView(frame: CGRect(x: 0, y: 0, width: width, height: headerHeight))..{ 325 | $0.backgroundColor = .systemBackground 326 | $0.clipsToBounds = false 327 | 328 | // hide top shaddow 329 | $0 += UIView(frame: CGRect(x: 0, y: headerHeight - 5, width: width, height: 5))..{ bot in 330 | bot.backgroundColor = .systemBackground 331 | bot.applyShadow() 332 | } 333 | $0 += UIView(frame: CGRect(x: 0, y: -20, width: width, height: headerHeight + 20))..{ top in 334 | top.backgroundColor = .systemBackground 335 | if scene == .main { 336 | top += UILabel(frame: CGRect(x: width/2, y: 17, width: width/4, height: headerHeight))..{ 337 | $0.text = "板块:" 338 | $0.textColor = .label 339 | $0.textAlignment = .right 340 | $0.font = UIFont.systemFont(ofSize: 12) 341 | } 342 | 343 | top += UIButton(frame: CGRect(x: width*3/4, y: 17, width: width/4 - 8, height: headerHeight))..{ btn in 344 | btn.addTarget(self, action: #selector(chooseBlock(_:)), for: .touchUpInside) 345 | btn.setTitle((d as! Thread.Manager).block.rawValue, for: .normal) 346 | btn.setDropDownStyle() 347 | 348 | dropDown.anchorView = btn 349 | dropDown.selectionAction = { (index: Int, item: String) in 350 | print("Selected item: \(item) at index: \(index)") 351 | btn.setTitle(item, for: .normal) 352 | (self.d as! Thread.Manager).block = Thread.Category.init(rawValue: item)! 353 | self.clearAll() 354 | } 355 | } 356 | } 357 | } 358 | } 359 | } 360 | 361 | func setReplyOrder(_ order: Order) { 362 | (d as! Floor.Manager)..{ 363 | if $0.order.netStr != order.netStr { 364 | $0.order = order 365 | clearAll() 366 | } 367 | } 368 | } 369 | 370 | func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int { 371 | d.count 372 | } 373 | 374 | func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell { 375 | // indexPath.row % 3 == 0 ? 376 | // tableView.dequeueReusableCell(withIdentifier: "FoldCell", for: indexPath) 377 | // : 378 | d.initializeCell(tableView.dequeueReusableCell(withIdentifier: "MainCell", for: indexPath) as! MainCell, index: indexPath.row).withVC(self) 379 | } 380 | 381 | func tableView(_ tableView: UITableView, estimatedHeightForRowAt indexPath: IndexPath) -> CGFloat { 382 | 210 383 | } 384 | 385 | func tableView(_ tableView: UITableView, heightForRowAt indexPath: IndexPath) -> CGFloat { 386 | UITableView.automaticDimension 387 | } 388 | 389 | func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) { 390 | !tableView.refreshControl!.isRefreshing && !tableView.mj_footer!.isRefreshing => { 391 | (navigationItem.largeTitleDisplayMode == .always && !(tableView.cellForRow(at: indexPath) as! MainCell).folded) => navigationItem.title = "" 392 | _ = d.didSelectedRow(self, index: indexPath.row) 393 | } 394 | } 395 | } 396 | 397 | extension MainVC: UIContextMenuInteractionDelegate { 398 | 399 | func contextMenuInteraction(_ interaction: UIContextMenuInteraction, configurationForMenuAtLocation location: CGPoint) -> UIContextMenuConfiguration? { 400 | return UIContextMenuConfiguration(identifier: nil) { () -> UIViewController? in 401 | UIViewController()..{ vc in 402 | vc.view += UIView(frame: CGRect(x: 0, y: 0, width: 50, height: 50))..{ 403 | $0.backgroundColor = .red 404 | vc.preferredContentSize = $0.frame.size 405 | } 406 | } 407 | } 408 | } 409 | 410 | 411 | } 412 | -------------------------------------------------------------------------------- /Forum/TableViews/MainVCMenu.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVCMenu.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/12/6. 6 | // 7 | 8 | import UIKit 9 | 10 | extension MainVC { 11 | 12 | func blockThread(_ msg: String, _ id: String, report: Bool, isViewing: Bool = true) { 13 | func commit(_ a: UIAlertAction) { 14 | DispatchQueue.global().async { 15 | var success = true 16 | report => success = Network.reportThread(for: id) 17 | DispatchQueue.main.async { 18 | if success { 19 | self.showAlert(msg, style: .success) { 20 | if !report { 21 | let li = G.blockedList.content 22 | !li.contains(id) => G.blockedList.content = li + [id] 23 | if isViewing { 24 | if let mvc = self.navigationController!.viewControllers[self.navigationController!.viewControllers.count - 2] as? MainVC, let dd = mvc.d as? Thread.Manager, let idx = dd.filtered.firstIndex(where: {$0.id == id}) { 25 | mvc.tableView.beginUpdates() 26 | dd.filter() 27 | mvc.tableView.deleteRows(at: [IndexPath(row: idx, section: 0)], with: .automatic) 28 | mvc.tableView.endUpdates() 29 | self.navigationController?.popViewController(animated: true) 30 | } 31 | } else { 32 | self.refresh() 33 | } 34 | } 35 | } 36 | } else { self.networkFailure() } 37 | } 38 | } 39 | } 40 | self << (UIAlertController( 41 | title: report ? "你确定要举报吗?" : "你确定要屏蔽吗?", 42 | message: report ? "受到举报较多的帖子会被隐藏,让我们共同维护良好的社区环境" : "您可以在本地选择屏蔽这个帖子", 43 | preferredStyle: .alert)..{ 44 | $0.addAction(.init(title: "确认", style: .destructive, handler: commit)) 45 | $0.addAction(.init(title: "取消", style: .cancel)) 46 | }) 47 | } 48 | 49 | func setTag(_ id: String, manager dd: Floor.Manager) { 50 | func commit(tag: Tag) { 51 | DispatchQueue.global().async { 52 | let success = Network.setTag(for: id, with: tag) 53 | DispatchQueue.main.async { 54 | if success { 55 | dd.thread.myTag = tag 56 | self.showAlert("设置成功", style: .success) 57 | } else { 58 | self.networkFailure() 59 | } 60 | } 61 | } 62 | } 63 | self << (UIAlertController(title: "建议标签", message: "在被多人设置后,帖子将会被打上该种标签,以便不同用户过滤阅读" + (dd.thread.myTag == nil ? "" : "\n\n您已经将其建议为\"\(dd.thread.myTag!.rawValue)\""), preferredStyle: .alert)..{ tagSelect in 64 | for cs in Tag.allCases { 65 | tagSelect.addAction(.init(title: cs.rawValue, style: .default, handler: { _ in 66 | commit(tag: cs) 67 | })) 68 | } 69 | tagSelect.addAction(.init(title: "取消", style: .cancel, handler: nil)) 70 | }) 71 | } 72 | 73 | @objc func firstBtnClicked(_ sender: Any) { 74 | if scene == .floors, let dd = d as? Floor.Manager { 75 | let al = UIAlertController(title: nil, message: nil, preferredStyle: .actionSheet) 76 | al.addAction(.init(title: "建议标签", style: .default, handler: { _ in 77 | self.setTag(dd.thread.id, manager: dd) 78 | })) 79 | al.addAction(.init(title: "屏蔽帖子", style: .default, handler: { _ in 80 | self.blockThread("屏蔽成功", dd.thread.id, report: false) 81 | })) 82 | al.addAction(.init(title: "举报帖子", style: .destructive, handler: { _ in 83 | self.blockThread("举报成功", dd.thread.id, report: true) 84 | })) 85 | al.addAction(.init(title: "取消", style: .cancel, handler: nil)) 86 | if let popoverPresentationController = al.popoverPresentationController { 87 | popoverPresentationController.sourceView = self.view 88 | let fr = self.navigationController!.navigationBar.frame 89 | popoverPresentationController.sourceRect = .init(x: fr.maxX - 1.0, y: fr.minY, width: 1.0, height: fr.height) 90 | popoverPresentationController.permittedArrowDirections = .init(rawValue: 0) 91 | } 92 | self << al 93 | } else { 94 | self.navigationItem.titleView = search 95 | search.searchBar.becomeFirstResponder() 96 | } 97 | } 98 | 99 | func tableView(_ tableView: UITableView, contextMenuConfigurationForRowAt indexPath: IndexPath, point: CGPoint) -> UIContextMenuConfiguration? { 100 | guard let cell = tableView.cellForRow(at: indexPath) as? MainCell else { return nil } 101 | if scene == .floors { 102 | let dd = self.d as! Floor.Manager 103 | return UIContextMenuConfiguration(identifier: nil, previewProvider: nil) {_ in 104 | UIMenu(title: "", children: [ 105 | UIAction(title: cell.liked == .disL ? "取消踩" : "踩", image: UIImage(systemName: "hand.thumbsdown"), identifier: nil, attributes: cell.liked != .like && !cell.folded ? [] : [.disabled], handler: { _ in 106 | cell.like(0) 107 | }), 108 | indexPath.row != 0 ? nil : 109 | UIAction(title: "建议标签", image: UIImage(systemName: "square.and.pencil"), identifier: nil, handler: { _ in 110 | self.setTag(dd.thread.id, manager: dd) 111 | }), 112 | indexPath.row != 0 ? nil : 113 | UIAction(title: "屏蔽帖子", image: UIImage(systemName: "eye.slash"), identifier: nil, handler: { _ in 114 | self.blockThread("屏蔽成功", cell.thread.id, report: false, isViewing: true) 115 | }), 116 | UIAction(title: indexPath.row == 0 ? "举报帖子" : "举报楼层", image: UIImage(systemName: "exclamationmark.triangle"), identifier: nil, attributes: !cell.folded ? [.destructive] : [.disabled, .destructive], handler: { _ in 117 | if indexPath.row == 0 { 118 | self.blockThread("举报成功", cell.thread.id, report: true, isViewing: true) 119 | } else { 120 | self << (UIAlertController.init(title: "你确定要举报吗?", message: "受到举报较多的楼层会被隐藏,让我们共同维护良好的社区环境", preferredStyle: .alert)..{ 121 | $0.addAction(.init(title: "确定", style: .destructive, handler: { _ in 122 | 123 | DispatchQueue.global().async { 124 | let success = Network.reportFloor(for: dd.thread.id, floor: cell.floor.id) 125 | DispatchQueue.main.async { 126 | if success { 127 | self.showAlert("举报成功", style: .success) 128 | } else { 129 | self.networkFailure() 130 | } 131 | } 132 | } 133 | })) 134 | $0.addAction(.init(title: "取消", style: .cancel, handler: nil)) 135 | }) 136 | } 137 | 138 | }) 139 | ].compactMap{$0}) 140 | } 141 | } else { 142 | return UIContextMenuConfiguration(identifier: nil) { 143 | (self.d.didSelectedRow(self, index: indexPath.row, commit: false) as! MainVC)..{ 144 | $0.inPreview = true 145 | } 146 | } actionProvider: {_ in 147 | UIMenu(title: "", children: [ 148 | UIAction(title: "屏蔽", image: UIImage(systemName: "eye.slash"), identifier: nil, handler: { (a) in 149 | self.blockThread("屏蔽成功", cell.thread.id, report: false, isViewing: false) 150 | }), 151 | UIAction(title: "举报", image: UIImage(systemName: "exclamationmark.triangle.fill"), identifier: nil, handler: { (a) in 152 | self.blockThread("举报成功", cell.thread.id, report: true, isViewing: false) 153 | }) 154 | ]) 155 | } 156 | } 157 | } 158 | 159 | func tableView(_ tableView: UITableView, willPerformPreviewActionForMenuWith configuration: UIContextMenuConfiguration, animator: UIContextMenuInteractionCommitAnimating) { 160 | if let dest = animator.previewViewController { 161 | animator.addAnimations { 162 | self.show(dest, sender: self) 163 | (dest as! MainVC)..{ vc in 164 | vc.inPreview = false 165 | vc.topDist.constant = vc._topDist 166 | vc.tableView.refreshControl = vc.refreshControl 167 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 168 | vc.updateFavour() 169 | } 170 | } 171 | } 172 | } 173 | } 174 | 175 | } 176 | -------------------------------------------------------------------------------- /Forum/TableViews/MainVCRefresh.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVCRefresh.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | extension MainVC { 11 | 12 | // func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) { 13 | // if indexPath.row >= d.count - 3 && !footer.isRefreshing && d.count > 10 { 14 | // print("start loadmore!", indexPath.row, d.count) 15 | // footer.beginRefreshing() 16 | // } 17 | // } 18 | 19 | func hasTappedAgain() { 20 | if tableView.refreshControl!.isRefreshing || tryDoubleTapping || firstLoading || isDoubleTapping { 21 | return 22 | } 23 | let y = self.tableView.refreshControl!.frame.maxY + self.tableView.adjustedContentInset.top 24 | let o = self.tableView.contentOffset.y 25 | 26 | if o < -30 || self.scene == .floors { 27 | self.isDoubleTapping = true 28 | self.tableView.setContentOffset(CGPoint(x: 0, y: self.tableView.contentOffset.y + 1), animated: true) 29 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 30 | self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated: true) 31 | DispatchQueue.main.asyncAfter(deadline: .now() + 0.05) { 32 | self.tableView.refreshControl?.beginRefreshing() 33 | // DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { 34 | self.refresh() 35 | // } 36 | } 37 | } 38 | } else { 39 | self.tryDoubleTapping = true 40 | self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated: true) 41 | } 42 | } 43 | 44 | func updateFavour() { 45 | if scene == .floors { 46 | navigationItem.rightBarButtonItems![2].setImageTo(UIImage(systemName: (d as! Floor.Manager).thread.hasFavoured ? "star.fill" : "star")) 47 | navigationItem.rightBarButtonItems![2].isEnabled = true 48 | print(">>> update favour", navigationItem.rightBarButtonItems![2].isEnabled) 49 | } 50 | } 51 | 52 | func clearAll(thenRefresh: Bool = true) { 53 | let prev = self.d.count 54 | _ = self.d.clear() 55 | self.tableView.mj_footer?.resetNoMoreData() 56 | footer.setTitle("正在加载...", for: .idle) 57 | tableView.beginUpdates() 58 | print("++++++++++++++ begin") 59 | if prev > self.d.count { 60 | tableView.deleteRows(at: (self.d.count.. self.tableView.beginUpdates() 102 | 103 | DispatchQueue.main.asyncAfter(deadline: .now() + (self.firstLoading ? 0.0 : 0.25)) { 104 | 105 | self.footer.setTitle("点击或上拉以加载更多", for: .idle) 106 | self.tableView.isScrollEnabled = true 107 | 108 | if self.isDoubleTapping { 109 | let y = self.tableView.refreshControl!.frame.maxY + self.tableView.adjustedContentInset.top 110 | self.tableView.setContentOffset(CGPoint(x: 0, y: -y), animated: true) 111 | self.isDoubleTapping = false 112 | } 113 | 114 | if prev > (self.scene == .floors ? 1 : 0) { 115 | print("RELOADING!!!") 116 | self.tableView.reloadData() 117 | } else { 118 | self.tableView.insertRows(at: (prev..= G.numberPerFetch { 128 | self.tableView.mj_footer?.resetNoMoreData() 129 | } else { 130 | self.tableView.mj_footer?.endRefreshingWithNoMoreData() 131 | self.footer.setTitle(count == -1 ? "加载失败" : "已经全部加载完毕", for: .noMoreData) 132 | } 133 | 134 | self.isRefreshing = false 135 | 136 | } 137 | } 138 | } 139 | } 140 | 141 | @objc func loadmore() { 142 | // self.tableView.isScrollEnabled = false 143 | DispatchQueue.global().async { 144 | usleep(100000) 145 | let prev = self.d.count 146 | let count = self.d.getContent() 147 | DispatchQueue.main.async { 148 | self.tableView.isScrollEnabled = false 149 | // self.tableView.bounces = false 150 | self.tableView.mj_footer?.endRefreshing() 151 | 152 | if self.d.count > prev { 153 | self.tableView.insertRows(at: (prev.. self.changeBar(hide: true) 28 | ?< self.changeBar(hide: false) 29 | #endif 30 | 31 | if scene == .floors { 32 | let ntitle = scrollView.contentOffset.y > 30 33 | ? "\((d as! Floor.Manager).thread.title)" 34 | : "#\((d as! Floor.Manager).thread.id)" 35 | if ntitle != navigationItem.title { 36 | navigationController?.navigationBar.layer.add( 37 | CATransition()..{ 38 | $0.duration = 0.2 39 | $0.type = .fade 40 | }, forKey: "fadeText") 41 | navigationItem.title = ntitle 42 | } 43 | } 44 | } 45 | 46 | func scrollViewDidEndScrollingAnimation(_ scrollView: UIScrollView) { 47 | self.tryDoubleTapping => { 48 | self.tryDoubleTapping = false 49 | self.hasTappedAgain() 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /Forum/TableViews/MainVCSearch.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVCSearch.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | extension MainVC: UISearchBarDelegate { 11 | func commitSearch() { 12 | if let tx = search.searchBar.text, let dd = d as? Thread.Manager { 13 | dd.searchFor = tx 14 | inSearchMode = true 15 | clearAll() 16 | } 17 | } 18 | 19 | func searchBarSearchButtonClicked(_ searchBar: UISearchBar) { 20 | searchBarCancelButtonClicked(searchBar) 21 | commitSearch() 22 | } 23 | 24 | func searchBarCancelButtonClicked(_ searchBar: UISearchBar) { 25 | self.view.endEditing(false) 26 | self.navigationItem.titleView = nil 27 | } 28 | } 29 | 30 | 31 | class SearchBarContainerView: UIView { 32 | 33 | let searchBar: UISearchBar 34 | 35 | init(customSearchBar: UISearchBar) { 36 | searchBar = customSearchBar 37 | super.init(frame: CGRect.zero) 38 | 39 | searchBar.placeholder = "Search" 40 | searchBar.barTintColor = UIColor.white 41 | searchBar.searchBarStyle = .minimal 42 | searchBar.returnKeyType = .done 43 | searchBar.showsCancelButton = true 44 | addSubview(searchBar) 45 | } 46 | 47 | override convenience init(frame: CGRect) { 48 | self.init(customSearchBar: UISearchBar()) 49 | self.frame = frame 50 | } 51 | 52 | required init?(coder aDecoder: NSCoder) { 53 | fatalError("init(coder:) has not been implemented") 54 | } 55 | 56 | override func layoutSubviews() { 57 | super.layoutSubviews() 58 | searchBar.frame = bounds 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /Forum/TableViews/MainVCText.swift: -------------------------------------------------------------------------------- 1 | // 2 | // MainVCText.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/11/30. 6 | // 7 | 8 | import UIKit 9 | 10 | extension MainVC: UITextViewDelegate { 11 | 12 | @objc func keyboardWillShow(_ sender: Notification) { 13 | if scene == .floors { 14 | let height = (sender.userInfo![UIResponder.keyboardFrameEndUserInfoKey]! as! NSValue).cgRectValue.height 15 | var time: TimeInterval = 0 16 | (sender.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey]! as! NSValue).getValue(&time) 17 | bottomViewHeight.constant = textViewHeight.constant + 16 + 10 + 16 18 | bottomSpace.constant = height 19 | UIView.animate(withDuration: time) { 20 | self.view.layoutIfNeeded() 21 | } 22 | completion: { (t) in 23 | if let dd = self.d as? Floor.Manager { 24 | let row = (dd.data.firstIndex(where: {$0.id == self.floor}) ?? -1) + 1 25 | if row > 0 { 26 | self.tableView.scrollToRow(at: IndexPath(row: row, section: 0), at: .none, animated: true) 27 | } 28 | } 29 | } 30 | } 31 | } 32 | 33 | @objc func keyboardWillHide(_ sender: Notification) { 34 | if scene == .floors { 35 | var time: TimeInterval = 0 36 | (sender.userInfo![UIResponder.keyboardAnimationDurationUserInfoKey]! as! NSValue).getValue(&time) 37 | bottomViewHeight.constant = textViewHeight.constant + 16 + 10 + 20 + 16 38 | let delta = bottomSpace.constant 39 | bottomSpace.constant = 0 40 | UIView.animate(withDuration: time) { 41 | self.tableView.contentOffset..{ 42 | self.tableView.setContentOffset(.init(x: 0, y: max(min($0.y - delta, self.tableView.contentSize.height), 0)), animated: false) 43 | } 44 | self.view.layoutIfNeeded() 45 | } 46 | } 47 | self.navigationItem.titleView = nil 48 | } 49 | 50 | func adjustTextView() { 51 | if scene == .floors { 52 | // if textView.text.count > 0 { 53 | let height = max(min(textView.contentSize.height, 100), 36) 54 | textViewHeight.constant = height 55 | bottomViewHeight.constant = textViewHeight.constant + 16 + 10 + 16 + (self.textView.isFirstResponder ? 0 : 20) 56 | UIView.animate(withDuration: 0.3) { 57 | self.view.layoutIfNeeded() 58 | } 59 | // } 60 | updateCountingLabel(label: replyCountLabel, text: textView.text, lineLimit: 20, charLimit: 817) 61 | } 62 | } 63 | 64 | @objc func keyboardDidHide(_ sender: Notification) { 65 | } 66 | 67 | func textViewDidChange(_ textView: UITextView) { 68 | adjustTextView() 69 | } 70 | // func textViewDidEndEditing(_ textView: UITextView) { 71 | // adjustTextView() 72 | // } 73 | // func textViewDidBeginEditing(_ textView: UITextView) { 74 | // adjustTextView() 75 | // } 76 | // func textViewDidChangeSelection(_ textView: UITextView) { 77 | // adjustTextView() 78 | // } 79 | 80 | @objc func viewTapped(_ sender: Any) { 81 | self.view.endEditing(false) 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /Forum/TableViews/SettingCell.swift: -------------------------------------------------------------------------------- 1 | // 2 | // SettingCell.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/12/5. 6 | // 7 | 8 | import UIKit 9 | 10 | class SettingCell: UITableViewCell { 11 | 12 | @IBOutlet weak var segment: UISegmentedControl! 13 | var forTag: Tag? 14 | var forThread: Bool = true 15 | 16 | override func awakeFromNib() { 17 | super.awakeFromNib() 18 | // Initialization code 19 | } 20 | 21 | override func setSelected(_ selected: Bool, animated: Bool) { 22 | super.setSelected(selected, animated: animated) 23 | 24 | // Configure the view for the selected state 25 | segment.addTarget(self, action: #selector(segDidChange(_:)), for: .valueChanged) 26 | } 27 | 28 | override func layoutSubviews() { 29 | super.layoutSubviews() 30 | 31 | } 32 | 33 | @objc func segDidChange(_ sender: Any) { 34 | if let t = forTag { 35 | var pr = G.viewStyle.content, npr = [String: Int]() 36 | for cs in Tag.allCases.map({String(describing: $0)}) { 37 | npr[cs] = pr[cs] ?? 0 38 | } 39 | npr[String(describing: t)] = segment.selectedSegmentIndex 40 | G.viewStyle.content = npr 41 | print(npr) 42 | } else { 43 | if forThread { 44 | G.threadStyle.content = segment.selectedSegmentIndex 45 | } else { 46 | G.floorStyle.content = segment.selectedSegmentIndex 47 | } 48 | } 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /Forum/TableViews/TabBarController.swift: -------------------------------------------------------------------------------- 1 | // 2 | // TabBarController.swift 3 | // Forum 4 | // 5 | // Created by Oscar on 2020/11/16. 6 | // 7 | 8 | import UIKit 9 | 10 | class TabBarController: UITabBarController { 11 | 12 | override func viewDidLoad() { 13 | super.viewDidLoad() 14 | 15 | // Do any additional setup after loading the view. 16 | let nav = UINavigationController(rootViewController: MainVC.new(.trends)) 17 | nav.navigationBar.prefersLargeTitles = true 18 | viewControllers?.insert(nav, at: 1) 19 | 20 | tabBar.items?[0].image = UIImage(systemName: "house") 21 | tabBar.items?[0].selectedImage = UIImage(systemName: "house.fill") 22 | tabBar.items?[0].title = "首页" 23 | 24 | tabBar.items?[1].image = UIImage(systemName: "crown") 25 | tabBar.items?[1].selectedImage = UIImage(systemName: "crown.fill") 26 | tabBar.items?[1].title = "趋势" 27 | 28 | tabBar.items?[2].image = UIImage(systemName: "person") 29 | tabBar.items?[2].selectedImage = UIImage(systemName: "person.fill") 30 | tabBar.items?[2].title = "我" 31 | } 32 | 33 | override func tabBar(_ tabBar: UITabBar, didSelect item: UITabBarItem) { 34 | if selectedIndex == tabBar.items!.firstIndex(of: item)! { 35 | let nav = viewControllers?[selectedIndex] as! UINavigationController 36 | if let vc = nav.viewControllers[0] as? DoubleTappable { 37 | vc.hasTappedAgain() 38 | } 39 | } 40 | } 41 | 42 | /* 43 | // MARK: - Navigation 44 | 45 | // In a storyboard-based application, you will often want to do a little preparation before navigation 46 | override func prepare(for segue: UIStoryboardSegue, sender: Any?) { 47 | // Get the new view controller using segue.destination. 48 | // Pass the selected object to the new view controller. 49 | } 50 | */ 51 | 52 | } 53 | -------------------------------------------------------------------------------- /ForumShare/ForumShare.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.network.client 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /ForumTests/ForumTests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForumTests.swift 3 | // ForumTests 4 | // 5 | // Created by Oscar on 2020/9/20. 6 | // 7 | 8 | import XCTest 9 | @testable import Forum 10 | 11 | class ForumTests: XCTestCase { 12 | 13 | override func setUpWithError() throws { 14 | // Put setup code here. This method is called before the invocation of each test method in the class. 15 | } 16 | 17 | override func tearDownWithError() throws { 18 | // Put teardown code here. This method is called after the invocation of each test method in the class. 19 | } 20 | 21 | func testExample() throws { 22 | // This is an example of a functional test case. 23 | // Use XCTAssert and related functions to verify your tests produce the correct results. 24 | } 25 | 26 | func testPerformanceExample() throws { 27 | // This is an example of a performance test case. 28 | self.measure { 29 | // Put the code you want to measure the time of here. 30 | } 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /ForumTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /ForumUITests/ForumUITests.swift: -------------------------------------------------------------------------------- 1 | // 2 | // ForumUITests.swift 3 | // ForumUITests 4 | // 5 | // Created by Oscar on 2020/9/20. 6 | // 7 | 8 | import XCTest 9 | 10 | class ForumUITests: XCTestCase { 11 | 12 | override func setUpWithError() throws { 13 | // Put setup code here. This method is called before the invocation of each test method in the class. 14 | 15 | // In UI tests it is usually best to stop immediately when a failure occurs. 16 | continueAfterFailure = false 17 | 18 | // In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this. 19 | } 20 | 21 | override func tearDownWithError() throws { 22 | // Put teardown code here. This method is called after the invocation of each test method in the class. 23 | } 24 | 25 | func testExample() throws { 26 | // UI tests must launch the application that they test. 27 | let app = XCUIApplication() 28 | app.launch() 29 | 30 | // Use recording to get started writing UI tests. 31 | // Use XCTAssert and related functions to verify your tests produce the correct results. 32 | } 33 | 34 | func testLaunchPerformance() throws { 35 | if #available(macOS 10.15, iOS 13.0, tvOS 13.0, *) { 36 | // This measures how long it takes to launch your application. 37 | measure(metrics: [XCTApplicationLaunchMetric()]) { 38 | XCUIApplication().launch() 39 | } 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /ForumUITests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | $(PRODUCT_BUNDLE_IDENTIFIER) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | $(PRODUCT_BUNDLE_PACKAGE_TYPE) 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleVersion 20 | 1 21 | 22 | 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Haichen Dong 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Podfile: -------------------------------------------------------------------------------- 1 | platform :ios, '11.0' 2 | use_frameworks! 3 | 4 | target 'Forum' do 5 | pod 'BlueSocket' 6 | pod 'Material', '~> 3.1.0' 7 | pod 'MJRefresh' 8 | pod 'UITextView+Placeholder' 9 | pod 'DropDown' 10 | pod 'MBProgressHUD', '~> 1.2.0' 11 | pod 'Socket.IO-Client-Swift', '~> 15.2.0' 12 | end 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Forum 2 | 3 | This is the iOS Frontend of SJTU Anonymous Forum [Wukefenggao](http://wukefenggao.cn), now available on the [App Store](http://wukefenggao.cn/download/iOS). 4 | 5 | Any issue reporting or suggestions are welcome, so please feel free to start a thread discussing it in the forum or simply submit a new issue here. 6 | 7 | The Android version can be found [here](https://github.com/TairanHe/SJTU-Anonymous_Forum). 8 | 9 | ### Acknowledgement 10 | 11 | Sincere gratitute to every member of our team, as well as every tester helping us to make the forum a better one. Many of the design were inspired by the Android implementation by [Tairan He](https://github.com/TairanHe) and the suggestions by [Yi Gu](https://github.com/wu-qing-157). 12 | 13 | --------------------------------------------------------------------------------