├── 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 |
--------------------------------------------------------------------------------