M Amalgamation.xcodeproj/project.pbxproj => Amalgamation.xcodeproj/project.pbxproj +40 -0
@@ 39,6 39,14 @@
0CB1D61B200DDD2A009515DA /* ddrsummer2017.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CB1D61A200DDD2A009515DA /* ddrsummer2017.json */; };
0CB1D61D200DE1F0009515DA /* TournamentStatsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CB1D61C200DE1F0009515DA /* TournamentStatsView.swift */; };
0CBA3C9720D0EC5600CAA62E /* PaddedLabel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBA3C9620D0EC5600CAA62E /* PaddedLabel.swift */; };
+ 0CBCD39521549E4E000412C2 /* DDR.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD39421549E4E000412C2 /* DDR.swift */; };
+ 0CBCD39721549FBC000412C2 /* ddrmatch.json in Resources */ = {isa = PBXBuildFile; fileRef = 0CBCD39621549FBC000412C2 /* ddrmatch.json */; };
+ 0CBCD3992154D319000412C2 /* DDRCardDrawViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD3982154D319000412C2 /* DDRCardDrawViewController.swift */; };
+ 0CBCD39B2155FE28000412C2 /* DDRCardView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD39A2155FE28000412C2 /* DDRCardView.swift */; };
+ 0CBCD39D2156191D000412C2 /* DispatchExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD39C2156191D000412C2 /* DispatchExtensions.swift */; };
+ 0CBCD39F2156C8FD000412C2 /* DDRCardDrawServer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD39E2156C8FD000412C2 /* DDRCardDrawServer.swift */; };
+ 0CBCD3A12156CA02000412C2 /* ServerOperation.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD3A02156CA02000412C2 /* ServerOperation.swift */; };
+ 0CBCD3A32156D263000412C2 /* TournamentScreen.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBCD3A22156D263000412C2 /* TournamentScreen.swift */; };
0CBDE8452082CE9000DDFB91 /* GeometryExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CBDE8442082CE9000DDFB91 /* GeometryExtensions.swift */; };
0CCC540820283C860086E340 /* PoissonUniform.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CCC540720283C860086E340 /* PoissonUniform.swift */; };
0CE5B8CE20C3B60D00ED8742 /* SettingsViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0CE5B8CD20C3B60D00ED8742 /* SettingsViewController.swift */; };
@@ 84,6 92,14 @@
0CB1D61A200DDD2A009515DA /* ddrsummer2017.json */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.json; path = ddrsummer2017.json; sourceTree = "<group>"; };
0CB1D61C200DE1F0009515DA /* TournamentStatsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentStatsView.swift; sourceTree = "<group>"; };
0CBA3C9620D0EC5600CAA62E /* PaddedLabel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PaddedLabel.swift; sourceTree = "<group>"; };
+ 0CBCD39421549E4E000412C2 /* DDR.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDR.swift; sourceTree = "<group>"; };
+ 0CBCD39621549FBC000412C2 /* ddrmatch.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = ddrmatch.json; sourceTree = "<group>"; };
+ 0CBCD3982154D319000412C2 /* DDRCardDrawViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRCardDrawViewController.swift; sourceTree = "<group>"; };
+ 0CBCD39A2155FE28000412C2 /* DDRCardView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRCardView.swift; sourceTree = "<group>"; };
+ 0CBCD39C2156191D000412C2 /* DispatchExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DispatchExtensions.swift; sourceTree = "<group>"; };
+ 0CBCD39E2156C8FD000412C2 /* DDRCardDrawServer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DDRCardDrawServer.swift; sourceTree = "<group>"; };
+ 0CBCD3A02156CA02000412C2 /* ServerOperation.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ServerOperation.swift; sourceTree = "<group>"; };
+ 0CBCD3A22156D263000412C2 /* TournamentScreen.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TournamentScreen.swift; sourceTree = "<group>"; };
0CBDE8442082CE9000DDFB91 /* GeometryExtensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeometryExtensions.swift; sourceTree = "<group>"; };
0CCC540720283C860086E340 /* PoissonUniform.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PoissonUniform.swift; sourceTree = "<group>"; };
0CE5B8CD20C3B60D00ED8742 /* SettingsViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsViewController.swift; sourceTree = "<group>"; };
@@ 164,6 180,7 @@
0C27C3E21F8AEFA5003A1D96 /* Source */ = {
isa = PBXGroup;
children = (
+ 0CBCD39321549E18000412C2 /* Rhythm Game Support */,
0C7A0CF91F8AF0CF00674AE7 /* Application */,
0C7A0D021F8AFFCD00674AE7 /* Challonge */,
0C7A0CFB1F8AF2CF00674AE7 /* Models */,
@@ 236,13 253,16 @@
children = (
0CF36018201F06D6009D4CE9 /* CollectionExtensions.swift */,
0C2E322A2026FA5F00FD0867 /* ColorExtensions.swift */,
+ 0CBCD39C2156191D000412C2 /* DispatchExtensions.swift */,
0C4181AD2042365300D14B33 /* FibonacciSphere.swift */,
0CBDE8442082CE9000DDFB91 /* GeometryExtensions.swift */,
0CA5FB41206B615600BB5803 /* MathUtil.swift */,
0CCC540720283C860086E340 /* PoissonUniform.swift */,
0C7A0D0A1F8B06FE00674AE7 /* Semaphore.swift */,
+ 0CBCD3A02156CA02000412C2 /* ServerOperation.swift */,
0C7A0D061F8B009300674AE7 /* StandardIO.swift */,
0C10EF54200D7F9A00285991 /* Theme.swift */,
+ 0CBCD3A22156D263000412C2 /* TournamentScreen.swift */,
0C28C329209EDC0A006F9FDE /* Transitionable.swift */,
0C7A0D071F8B009300674AE7 /* TVSupport.swift */,
);
@@ 253,10 273,22 @@
isa = PBXGroup;
children = (
0CB1D61A200DDD2A009515DA /* ddrsummer2017.json */,
+ 0CBCD39621549FBC000412C2 /* ddrmatch.json */,
);
path = "Test Data";
sourceTree = "<group>";
};
+ 0CBCD39321549E18000412C2 /* Rhythm Game Support */ = {
+ isa = PBXGroup;
+ children = (
+ 0CBCD39421549E4E000412C2 /* DDR.swift */,
+ 0CBCD39E2156C8FD000412C2 /* DDRCardDrawServer.swift */,
+ 0CBCD3982154D319000412C2 /* DDRCardDrawViewController.swift */,
+ 0CBCD39A2155FE28000412C2 /* DDRCardView.swift */,
+ );
+ path = "Rhythm Game Support";
+ sourceTree = "<group>";
+ };
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
@@ 316,6 348,7 @@
buildActionMask = 2147483647;
files = (
0CAEB90B1F8B0C8100B0B246 /* Localizable.strings in Resources */,
+ 0CBCD39721549FBC000412C2 /* ddrmatch.json in Resources */,
0CB1D618200DCCE4009515DA /* Exan-Regular.ttf in Resources */,
0C10EF52200D7DCC00285991 /* Karla-BoldItalic.ttf in Resources */,
0C10EF53200D7DCC00285991 /* Karla-Bold.ttf in Resources */,
@@ 333,16 366,21 @@
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
+ 0CBCD39B2155FE28000412C2 /* DDRCardView.swift in Sources */,
+ 0CBCD39521549E4E000412C2 /* DDR.swift in Sources */,
0CB1D61D200DE1F0009515DA /* TournamentStatsView.swift in Sources */,
0CF36019201F06D6009D4CE9 /* CollectionExtensions.swift in Sources */,
0CAEB90E1F8B0E7A00B0B246 /* StandardIO.swift in Sources */,
0C7A0D041F8AFFE900674AE7 /* ChallongeServer.swift in Sources */,
+ 0CBCD3992154D319000412C2 /* DDRCardDrawViewController.swift in Sources */,
+ 0CBCD39D2156191D000412C2 /* DispatchExtensions.swift in Sources */,
0C27C3EC1F8AEFB8003A1D96 /* TournamentViewController.swift in Sources */,
0C2E322D2027022F00FD0867 /* VisualizationViewController.swift in Sources */,
0C3212B020170730000FD6D0 /* TournamentBracketView.swift in Sources */,
0CCC540820283C860086E340 /* PoissonUniform.swift in Sources */,
0CAEB90F1F8B0E7E00B0B246 /* TVSupport.swift in Sources */,
0C3212B220170788000FD6D0 /* TournamentMatchView.swift in Sources */,
+ 0CBCD3A32156D263000412C2 /* TournamentScreen.swift in Sources */,
0C10EF55200D7F9A00285991 /* Theme.swift in Sources */,
0CE85C552022E1F000CC726D /* TournamentBracketAnnotationsView.swift in Sources */,
0C28C32C209FE960006F9FDE /* InterstitialView.swift in Sources */,
@@ 350,6 388,8 @@
0CBA3C9720D0EC5600CAA62E /* PaddedLabel.swift in Sources */,
0CBDE8452082CE9000DDFB91 /* GeometryExtensions.swift in Sources */,
0C27C3EB1F8AEFB6003A1D96 /* AppDelegate.swift in Sources */,
+ 0CBCD39F2156C8FD000412C2 /* DDRCardDrawServer.swift in Sources */,
+ 0CBCD3A12156CA02000412C2 /* ServerOperation.swift in Sources */,
0CE5B8D020C3C30800ED8742 /* SavedTournament.swift in Sources */,
0C7A0CFD1F8AF2E400674AE7 /* Participant.swift in Sources */,
0C7A0CFF1F8AF73D00674AE7 /* Match.swift in Sources */,
A Amalgamation/Resources/Assets.xcassets/CardDraw.imageset/CardDraw.pdf => Amalgamation/Resources/Assets.xcassets/CardDraw.imageset/CardDraw.pdf +0 -0
A Amalgamation/Resources/Assets.xcassets/CardDraw.imageset/Contents.json => Amalgamation/Resources/Assets.xcassets/CardDraw.imageset/Contents.json +21 -0
@@ 0,0 1,21 @@
+{
+ "images" : [
+ {
+ "idiom" : "universal",
+ "filename" : "CardDraw.pdf",
+ "scale" : "1x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "2x"
+ },
+ {
+ "idiom" : "universal",
+ "scale" : "3x"
+ }
+ ],
+ "info" : {
+ "version" : 1,
+ "author" : "xcode"
+ }
+}<
\ No newline at end of file
A Amalgamation/Resources/Test Data/ddrmatch.json => Amalgamation/Resources/Test Data/ddrmatch.json +88 -0
@@ 0,0 1,88 @@
+{
+ "match" : {
+ "playerOneName" : "iamchris4life",
+ "playerTwoName" : "rogerclark",
+ "cardDraws" : [
+ {
+ "id" : 1337,
+ "title" : "Pluto Relinquish",
+ "artist" : "2MB",
+ "bpm" : "800",
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "challenge",
+ "rating" : 15
+ }
+ },
+ {
+ "id" : 573,
+ "title" : "MAX 300",
+ "artist" : "Ω",
+ "bpm" : "300",
+ "vetoed" : true,
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "expert",
+ "rating" : 12
+ }
+ },
+ {
+ "id" : 420,
+ "title" : "Love You More",
+ "artist" : "BEMANI Sound Team 'Sota F.'",
+ "bpm" : "175",
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "expert",
+ "rating" : 16
+ }
+ },
+
+ {
+ "id" : 1337,
+ "title" : "Pluto Relinquish",
+ "artist" : "2MB",
+ "bpm" : "800",
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "challenge",
+ "rating" : 15
+ }
+ },
+ {
+ "id" : 573,
+ "title" : "MAX 300",
+ "artist" : "Ω",
+ "bpm" : "300",
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "expert",
+ "rating" : 12
+ }
+ },
+ {
+ "id" : 420,
+ "title" : "お米の美味しい炊き方、そしてお米を食べることによるその効果。",
+ "artist" : "BEMANI Sound Team 'Sota F.'",
+ "bpm" : "175",
+ "vetoed" : true,
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "expert",
+ "rating" : 16
+ }
+ },
+ {
+ "id" : 420,
+ "title" : "Love You More",
+ "artist" : "BEMANI Sound Team 'Sota F.'",
+ "bpm" : "175",
+ "chart" : {
+ "plurality" : "single",
+ "difficulty" : "expert",
+ "rating" : 16
+ }
+ }
+ ]
+ }
+}
M Amalgamation/Source/Challonge/ChallongeServer.swift => Amalgamation/Source/Challonge/ChallongeServer.swift +0 -49
@@ 106,55 106,6 @@ class ChallongeServer
}
}
-internal class ServerOperation : Operation
-{
- var baseURL: URL
- var session: URLSession
- internal(set) var error: Error?
-
- init(baseURL: URL, session: URLSession)
- {
- self.baseURL = baseURL
- self.session = session
- }
-
- func fetchData(_ url: URL) -> Data?
- {
- var fetchedData: Data?
-
- let semaphore = Semaphore(value: 0)
- let task = self.session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
- fetchedData = data
- self.error = error
-
- semaphore.signal()
- })
- task.resume()
-
- semaphore.wait()
- return fetchedData
- }
-
- func fetchResponse(_ request: URLRequest) -> (Data?, URLResponse?)
- {
- var fetchedData: Data?
- var fetchedResponse: URLResponse?
-
- let semaphore = Semaphore(value: 0)
- let task = self.session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
- fetchedResponse = response
- fetchedData = data
- self.error = error
-
- semaphore.signal()
- })
- task.resume()
-
- semaphore.wait()
- return (fetchedData, fetchedResponse)
- }
-}
-
internal class LoadTournamentOperation : ServerOperation
{
var tournamentStringID: String = ""
A Amalgamation/Source/Rhythm Game Support/DDR.swift => Amalgamation/Source/Rhythm Game Support/DDR.swift +47 -0
@@ 0,0 1,47 @@
+//
+// DDR.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/20/18.
+//
+
+import Foundation
+
+struct DDRChart : Codable
+{
+ enum Plurality : String, Codable
+ {
+ case single
+ case double
+ }
+
+ enum Difficulty : String, Codable
+ {
+ case beginner
+ case basic
+ case difficult
+ case expert
+ case challenge
+ }
+
+ var plurality: Plurality = .single
+ var difficulty: Difficulty = .beginner
+ var rating: Int = 0
+}
+
+struct DDRSong : Codable
+{
+ var id: Int?
+ var title: String = ""
+ var artist: String = ""
+ var bpm: String?
+ var chart: DDRChart?
+ var vetoed: Bool?
+}
+
+struct DDRMatch : Codable
+{
+ var playerOneName: String?
+ var playerTwoName: String?
+ var cardDraws: [DDRSong] = []
+}
A Amalgamation/Source/Rhythm Game Support/DDRCardDrawServer.swift => Amalgamation/Source/Rhythm Game Support/DDRCardDrawServer.swift +106 -0
@@ 0,0 1,106 @@
+//
+// DDRCardDrawServer.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/22/18.
+//
+
+import Foundation
+
+struct DDRCardDrawError : Error
+{
+ enum Code
+ {
+ case unknown
+ case connectionFailure
+ case noMatchAvailable
+ case parseFailure
+ }
+
+ let code: Code
+
+ init(_ code: Code)
+ {
+ self.code = code
+ }
+}
+
+class DDRCardDrawServer
+{
+ private var _urlSession: URLSession
+ private var _operationQueue: OperationQueue
+
+ fileprivate static let _baseURL = URL(string: "http://mephisto.zanneth.com:5730")!
+
+ init()
+ {
+ let config = URLSessionConfiguration.default
+ _urlSession = URLSession(configuration: config)
+
+ _operationQueue = OperationQueue()
+ _operationQueue.maxConcurrentOperationCount = 1
+ }
+
+ func fetchCurrentMatch(completion: @escaping (DDRMatch?, Error?) -> Void)
+ {
+ let operation = LoadDDRMatchOperation(baseURL: DDRCardDrawServer._baseURL, session: _urlSession)
+
+ weak var weakOp = operation
+ operation.completionBlock = {
+ guard let strongOp = weakOp else { completion(nil, nil) ; return }
+ if let error = strongOp.error {
+ self._logError("failed to load matches", error: error)
+ }
+
+ completion(strongOp.ddrMatch, strongOp.error)
+ }
+
+ _operationQueue.addOperation(operation)
+ }
+
+ // MARK: Internal
+
+ internal func _logError(_ description: String, error: Error)
+ {
+ #if DEBUG
+ StandardErrorOutputStream.shared.write("ERROR: \(description) \(error.localizedDescription)\n")
+ #endif
+ }
+}
+
+internal class LoadDDRMatchOperation : ServerOperation
+{
+ internal(set) var ddrMatch: DDRMatch?
+
+ override func main()
+ {
+ let urlRequest = URLRequest(url: self.baseURL)
+ let (data, response) = self.fetchResponse(urlRequest)
+
+ var errorCode: DDRCardDrawError.Code?
+ if let httpResponse = response as? HTTPURLResponse {
+ switch (httpResponse.statusCode) {
+ case 200:
+ errorCode = nil
+ case 404:
+ errorCode = .noMatchAvailable
+ default:
+ errorCode = .unknown
+ }
+ } else {
+ errorCode = .connectionFailure
+ }
+
+ if let errorCode = errorCode {
+ self.error = DDRCardDrawError(errorCode)
+ } else {
+ do {
+ let decoder = JSONDecoder()
+ let jsonObject = try decoder.decode([String : DDRMatch].self, from: data!)
+ self.ddrMatch = jsonObject["match"]
+ } catch {
+ self.error = DDRCardDrawError(.parseFailure)
+ }
+ }
+ }
+}
A Amalgamation/Source/Rhythm Game Support/DDRCardDrawViewController.swift => Amalgamation/Source/Rhythm Game Support/DDRCardDrawViewController.swift +343 -0
@@ 0,0 1,343 @@
+//
+// DDRCardDrawViewController.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/21/18.
+//
+
+import Foundation
+import UIKit
+
+class DDRCardDrawViewController : UIViewController, CustomTournamentScreen, Transitionable
+{
+ private let _server: DDRCardDrawServer = DDRCardDrawServer()
+ private var _cachedUpdate: DDRMatch?
+
+ private let _kanjiLabel: UILabel = UILabel()
+ private let _playerOneLabel: UILabel = UILabel()
+ private let _playerTwoLabel: UILabel = UILabel()
+ private let _versusLabel: UILabel = UILabel()
+
+ private var _cardViews: [DDRCardView] = []
+ private var _transitionInProgress: Bool = false
+ private var _active: Bool = false
+
+ // MARK: API
+
+ var ddrMatch: DDRMatch?
+ {
+ didSet
+ {
+ _reloadPlayersUI()
+ _reloadCardViews()
+ }
+ }
+
+ func reloadData()
+ {
+ _server.fetchCurrentMatch { (match: DDRMatch?, error: Error?) in
+ DispatchQueue.main.async {
+ self._cachedUpdate = match
+
+ if !self._transitionInProgress {
+ self._reloadFromCachedResult()
+ }
+ }
+ }
+ }
+
+ // MARK: UIViewController
+
+ override func viewDidLoad()
+ {
+ super.viewDidLoad()
+
+ let theme = Theme.mainTheme
+ let headerFont = theme.headerTextAttributes[NSAttributedStringKey.font] as! UIFont
+ let centeredParagraphStyle = NSMutableParagraphStyle()
+ centeredParagraphStyle.alignment = .center
+
+ let kanjiFontDescriptor = headerFont.fontDescriptor.withSymbolicTraits(.traitBold)!
+ let kanjiFont = UIFont(descriptor: kanjiFontDescriptor, size: 72.0)
+ let kanjiAttributes: TextAttributes = [
+ NSAttributedStringKey.paragraphStyle : centeredParagraphStyle,
+ NSAttributedStringKey.font : kanjiFont,
+ NSAttributedStringKey.foregroundColor : UIColor.white
+ ]
+ _kanjiLabel.attributedText = NSAttributedString(string: "試合状態", attributes: kanjiAttributes)
+
+ let view = self.view
+ view?.addSubview(_kanjiLabel)
+ view?.addSubview(_playerOneLabel)
+ view?.addSubview(_playerTwoLabel)
+ view?.addSubview(_versusLabel)
+ }
+
+ override func viewDidLayoutSubviews()
+ {
+ super.viewDidLayoutSubviews()
+
+ let bounds = self.view.bounds
+ let kanjiPlayerNamesVPadding = CGFloat(20.0)
+ let playerUICardsVPadding = bounds.size.height / 5.0
+
+ // pre-compute sizes
+ let kanjiLabelSize = _kanjiLabel.sizeThatFits(bounds.size)
+ let collinearLabels = [_playerOneLabel, _versusLabel, _playerTwoLabel]
+ var playerLabelsBoundingSize = CGSize(width: 0.0, height: 0.0)
+
+ for label in collinearLabels {
+ let labelSize = label.sizeThatFits(bounds.size)
+ playerLabelsBoundingSize.width += labelSize.width
+ playerLabelsBoundingSize.height = max(playerLabelsBoundingSize.height, labelSize.height)
+ }
+
+ let cardViewsWidth = rint(bounds.size.width / 9.0)
+ let cardViewsSize = CGSize(
+ width: cardViewsWidth,
+ height: rint(cardViewsWidth * 1.61803) // golden ratio
+ )
+
+ let viewsTotalHeight = (
+ kanjiLabelSize.height +
+ kanjiPlayerNamesVPadding +
+ playerLabelsBoundingSize.height +
+ playerUICardsVPadding +
+ cardViewsSize.height
+ )
+ let viewsOriginY = rint(bounds.origin.y + (bounds.size.height / 2.0 - viewsTotalHeight / 2.0))
+
+ // layout kanji label
+ let kanjiLabelFrame = CGRect(
+ x: rint(bounds.origin.x + (bounds.size.width / 2.0 - kanjiLabelSize.width / 2.0)),
+ y: viewsOriginY,
+ width: kanjiLabelSize.width,
+ height: kanjiLabelSize.height
+ )
+ _kanjiLabel.frame = kanjiLabelFrame
+
+ // layout player labels
+ let labelsHPadding = CGFloat(8.0)
+ let labelsOriginX = rint(bounds.origin.x + (bounds.size.width / 2.0 - playerLabelsBoundingSize.width / 2.0))
+ var previousLabel: UILabel?
+
+ for label in collinearLabels {
+ let labelSize = label.sizeThatFits(bounds.size)
+ let originX = ((previousLabel != nil) ? previousLabel!.frame.maxX + labelsHPadding : labelsOriginX)
+ let labelFrame = CGRect(
+ x: originX,
+ y: kanjiLabelFrame.maxY + kanjiPlayerNamesVPadding,
+ width: labelSize.width,
+ height: labelSize.height
+ )
+ label.frame = labelFrame
+ previousLabel = label
+ }
+
+ // layout card views
+ let cardViewsHPadding = CGFloat(20.0)
+ let cardViewsCount = CGFloat(_cardViews.count)
+ let cardViewsTotalWidth = (cardViewsCount * cardViewsSize.width) + ((cardViewsCount - 1.0) * cardViewsHPadding)
+ let cardViewsOriginX = rint(bounds.origin.x + (bounds.size.width / 2.0 - cardViewsTotalWidth / 2.0))
+ let cardViewsOriginY = rint(previousLabel!.frame.maxY + playerUICardsVPadding)
+
+ var lastCardViewFrame: CGRect?
+ for cardView in _cardViews {
+ let originX = ((lastCardViewFrame != nil) ? lastCardViewFrame!.maxX + cardViewsHPadding : cardViewsOriginX)
+ let cardViewFrame = CGRect(
+ x: originX,
+ y: cardViewsOriginY,
+ width: cardViewsSize.width,
+ height: cardViewsSize.height
+ )
+
+ cardView.frame = cardViewFrame
+ lastCardViewFrame = cardViewFrame
+ }
+ }
+
+ // MARK: CustomTournamentScreen
+
+ var shouldShowScreen: Bool
+ {
+ get
+ {
+ if (self.ddrMatch == nil) {
+ self.reloadData()
+ }
+ return (self.ddrMatch != nil)
+ }
+ }
+
+ // MARK: Transitionable
+
+ func prepareToTransitionToActive()
+ {
+ self.reloadData()
+ }
+
+ func transitionToActive(transition: Transition, completion: @escaping (Bool) -> Void)
+ {
+ _transitionToActive(active: true, transition: transition, completion: completion)
+ }
+
+ func transitionToInactive(transition: Transition, completion: @escaping (Bool) -> Void)
+ {
+ _transitionToActive(active: false, transition: transition, completion: completion)
+ }
+
+ // MARK: Internal
+
+ internal func _reloadPlayersUI()
+ {
+ let unknownPlayerString = NSLocalizedString("MATCH_UNKNOWN_PLAYER_NAME", comment: "")
+ let playerOneName = (self.ddrMatch?.playerOneName ?? unknownPlayerString).uppercased()
+ let playerTwoName = (self.ddrMatch?.playerTwoName ?? unknownPlayerString).uppercased()
+
+ let theme = Theme.mainTheme
+ var versusTextAttributes = theme.headerTextAttributes
+ let headerTextFont = versusTextAttributes[NSAttributedStringKey.font] as! UIFont
+ let boldHeaderTextFontDesc = headerTextFont.fontDescriptor.withSymbolicTraits(.traitBold)!
+ versusTextAttributes[NSAttributedStringKey.font] = UIFont(descriptor: boldHeaderTextFontDesc, size: 55.0)
+
+ var playerNameAttributes = versusTextAttributes
+ playerNameAttributes[NSAttributedStringKey.foregroundColor] = theme.colorPalette.primaryColor
+
+ _playerOneLabel.attributedText = NSAttributedString(string: playerOneName, attributes: playerNameAttributes)
+ _playerTwoLabel.attributedText = NSAttributedString(string: playerTwoName, attributes: playerNameAttributes)
+ _versusLabel.attributedText = NSAttributedString(string: "vs.", attributes: versusTextAttributes)
+
+ self.view.setNeedsLayout()
+ }
+
+ internal func _reloadCardViews()
+ {
+ _cardViews.forEach { $0.removeFromSuperview() }
+ _cardViews.removeAll()
+
+ let songs = self.ddrMatch?.cardDraws ?? []
+ for song in songs {
+ let cardView = DDRCardView()
+ cardView.song = song
+ cardView.layer.opacity = (_active ? 1.0 : 0.0)
+
+ _cardViews.append(cardView)
+ self.view.addSubview(cardView)
+ }
+
+ self.view.setNeedsLayout()
+ }
+
+ internal func _reloadFromCachedResult()
+ {
+ self.ddrMatch = _cachedUpdate
+ }
+
+ internal func _inTransaction_transitionPlayersUI(active: Bool, transition: Transition)
+ {
+ let playersUILabels = [_kanjiLabel, _playerOneLabel, _playerTwoLabel, _versusLabel]
+ playersUILabels.forEach { $0.layer.removeAllAnimations() }
+
+ let playerLabelsOffset = CGFloat(20.0)
+ var animations: [CAAnimation] = []
+
+ let fadeAnimation = CABasicAnimation(keyPath: "opacity")
+ fadeAnimation.fromValue = CGFloat(active ? 0.0 : 1.0)
+ fadeAnimation.toValue = CGFloat(active ? 1.0 : 0.0)
+ animations.append(fadeAnimation)
+
+ let playerOneAnimation = CABasicAnimation(keyPath: "transform.translation.x")
+ playerOneAnimation.fromValue = (active ? -playerLabelsOffset : 0.0)
+ playerOneAnimation.toValue = (active ? 0.0 : -playerLabelsOffset)
+ animations.append(playerOneAnimation)
+
+ let playerTwoAnimation = CABasicAnimation(keyPath: "transform.translation.x")
+ playerTwoAnimation.fromValue = (active ? playerLabelsOffset : 0.0)
+ playerTwoAnimation.toValue = (active ? 0.0 : playerLabelsOffset)
+ animations.append(playerTwoAnimation)
+
+ playersUILabels.forEach { $0.layer.opacity = (active ? 0.0 : 1.0) }
+
+ // set common properties on the animations
+ for animation in animations {
+ animation.duration = transition.duration
+ animation.beginTime = CACurrentMediaTime() + transition.delay
+ animation.timingFunction = CAMediaTimingFunction(name: (active ? kCAMediaTimingFunctionEaseOut : kCAMediaTimingFunctionEaseIn))
+ }
+
+ playersUILabels.forEach { $0.layer.add(fadeAnimation, forKey: nil) }
+ _playerOneLabel.layer.add(playerOneAnimation, forKey: nil)
+ _playerTwoLabel.layer.add(playerTwoAnimation, forKey: nil)
+
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.secondsFromNow(transition.delay)) {
+ playersUILabels.forEach { $0.layer.opacity = (active ? 1.0 : 0.0) }
+ }
+ }
+
+ internal func _inTransaction_transitionCardViews(active: Bool, transition: Transition)
+ {
+ _cardViews.forEach { $0.layer.removeAllAnimations() }
+
+ let cardViewsOffset = CGFloat(100.0)
+ let timingFunction = CAMediaTimingFunction(name: (active ? kCAMediaTimingFunctionEaseOut : kCAMediaTimingFunctionEaseIn))
+ var animations: [CAAnimation] = []
+
+ let fadeAnimation = CABasicAnimation(keyPath: "opacity")
+ fadeAnimation.fromValue = CGFloat(active ? 0.0 : 1.0)
+ fadeAnimation.toValue = CGFloat(active ? 1.0 : 0.0)
+ fadeAnimation.duration = transition.duration
+ fadeAnimation.timingFunction = timingFunction
+ fadeAnimation.fillMode = kCAFillModeForwards
+ animations.append(fadeAnimation)
+
+ let transformAnimation = CABasicAnimation(keyPath: "transform.translation.y")
+ transformAnimation.fromValue = (active ? cardViewsOffset : 0.0)
+ transformAnimation.toValue = (active ? 0.0 : cardViewsOffset)
+ transformAnimation.duration = transition.duration
+ transformAnimation.timingFunction = timingFunction
+ transformAnimation.fillMode = kCAFillModeForwards
+ animations.append(transformAnimation)
+
+ _cardViews.forEach { $0.layer.opacity = (active ? 0.0 : 1.0) }
+
+ let delayStagger = 0.1
+ var delayOffset = 0.0
+ for cardView in _cardViews {
+ let beginTime = CACurrentMediaTime() + transition.delay + delayOffset
+
+ let cardTransformAnimation = transformAnimation.copy() as! CABasicAnimation
+ cardTransformAnimation.beginTime = beginTime
+
+ let cardFadeAnimation = fadeAnimation.copy() as! CABasicAnimation
+ cardFadeAnimation.beginTime = beginTime
+
+ cardView.layer.add(cardFadeAnimation, forKey: nil)
+ cardView.layer.add(cardTransformAnimation, forKey: nil)
+
+ delayOffset += delayStagger
+
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.secondsFromNow(transition.delay + delayOffset)) {
+ cardView.layer.opacity = (active ? 1.0 : 0.0)
+ }
+ }
+ }
+
+ internal func _transitionToActive(active: Bool, transition: Transition, completion: @escaping (Bool) -> Void)
+ {
+ _transitionInProgress = true
+
+ CATransaction.begin()
+ CATransaction.setCompletionBlock {
+ completion(true)
+
+ self._active = active
+ self._transitionInProgress = false
+ self._reloadFromCachedResult()
+ }
+
+ _inTransaction_transitionPlayersUI(active: active, transition: transition)
+ _inTransaction_transitionCardViews(active: active, transition: transition)
+
+ CATransaction.commit()
+ }
+}
A Amalgamation/Source/Rhythm Game Support/DDRCardView.swift => Amalgamation/Source/Rhythm Game Support/DDRCardView.swift +179 -0
@@ 0,0 1,179 @@
+//
+// DDRCardView.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/21/18.
+//
+
+import Foundation
+import UIKit
+
+class DDRCardView : UIView
+{
+ private let _containerView: UIView = UIView()
+ private let _titleLabel: UILabel = UILabel()
+ private let _artistLabel: UILabel = UILabel()
+ private let _bpmLabel: UILabel = UILabel()
+ private let _difficultyLabel: UILabel = UILabel()
+
+ override init(frame: CGRect)
+ {
+ super.init(frame: frame)
+
+ self.addSubview(_containerView)
+
+ let textAttributes = Theme.mainTheme.bodyTextAttributes
+ let font = textAttributes[NSAttributedStringKey.font] as! UIFont
+ let textColor = textAttributes[NSAttributedStringKey.foregroundColor] as! UIColor
+
+ // set common properties, add labels as subviews
+ for label in [_titleLabel, _artistLabel, _bpmLabel, _difficultyLabel] {
+ label.font = font
+ label.textColor = textColor
+
+ _containerView.addSubview(label)
+ }
+
+ let boldFontDescriptor = font.fontDescriptor.withSymbolicTraits(.traitBold)!
+ _titleLabel.numberOfLines = 2
+ _titleLabel.font = UIFont(descriptor: boldFontDescriptor, size: 28.0)
+ _titleLabel.textAlignment = .center
+
+ _artistLabel.numberOfLines = 2
+ _artistLabel.font = font.withSize(22.0)
+ _artistLabel.textAlignment = .center
+ }
+
+ required init?(coder aDecoder: NSCoder)
+ {
+ fatalError("unsupported")
+ }
+
+ // MARK: Accessors
+
+ var song: DDRSong?
+ {
+ didSet
+ {
+ _titleLabel.text = song?.title
+ _artistLabel.text = song?.artist
+ _bpmLabel.text = String(format: "%@ BPM", (song?.bpm ?? "?"))
+ _difficultyLabel.text = song?.chart?.difficultyDescription
+
+ _containerView.backgroundColor = song?.chart?.difficultyColor
+
+ if song?.vetoed ?? false {
+ _containerView.alpha = 0.5
+ } else {
+ _containerView.alpha = 1.0
+ }
+
+ self.setNeedsLayout()
+ }
+ }
+
+ // MARK: UIView
+
+ override func layoutSubviews()
+ {
+ super.layoutSubviews()
+
+ let bounds = self.bounds
+ let edgePadding = CGFloat(5.0)
+ let titleArtistPadding = CGFloat(5.0)
+
+ _containerView.frame = bounds
+
+ let titleLabelSize = _titleLabel.sizeThatFits(bounds.size)
+ let artistLabelSize = _artistLabel.sizeThatFits(bounds.size)
+ let titleArtistTotalHeight = (
+ titleLabelSize.height +
+ titleArtistPadding +
+ artistLabelSize.height
+ )
+
+ let titleLabelFrame = CGRect(
+ x: rint(bounds.size.width / 2.0 - titleLabelSize.width / 2.0),
+ y: rint(bounds.size.height / 2.0 - titleArtistTotalHeight / 2.0),
+ width: titleLabelSize.width,
+ height: titleLabelSize.height
+ )
+ _titleLabel.frame = titleLabelFrame
+
+ let artistLabelFrame = CGRect(
+ x: rint(bounds.size.width / 2.0 - artistLabelSize.width / 2.0),
+ y: titleLabelFrame.maxY + titleArtistPadding,
+ width: artistLabelSize.width,
+ height: artistLabelSize.height
+ )
+ _artistLabel.frame = artistLabelFrame
+
+ let bpmLabelSize = _bpmLabel.sizeThatFits(bounds.size)
+ let bpmLabelFrame = CGRect(
+ x: edgePadding,
+ y: rint(bounds.size.height - edgePadding - bpmLabelSize.height),
+ width: bpmLabelSize.width,
+ height: bpmLabelSize.height
+ )
+ _bpmLabel.frame = bpmLabelFrame
+
+ let difficultyLabelSize = _difficultyLabel.sizeThatFits(bounds.size)
+ let difficultyLabelFrame = CGRect(
+ x: rint(bounds.size.width - edgePadding - difficultyLabelSize.width),
+ y: rint(bounds.size.height - edgePadding - bpmLabelSize.height),
+ width: difficultyLabelSize.width,
+ height: difficultyLabelSize.height
+ )
+ _difficultyLabel.frame = difficultyLabelFrame
+ }
+}
+
+extension DDRChart
+{
+ var difficultyDescription: String
+ {
+ get
+ {
+ var difficultyKey: String!
+
+ switch (self.difficulty) {
+ case .beginner:
+ difficultyKey = "DDR_DIFFICULTY_SHORT_BEGINNER"
+ case .basic:
+ difficultyKey = "DDR_DIFFICULTY_SHORT_BASIC"
+ case .difficult:
+ difficultyKey = "DDR_DIFFICULTY_SHORT_DIFFICULT"
+ case .expert:
+ difficultyKey = "DDR_DIFFICULTY_SHORT_EXPERT"
+ case .challenge:
+ difficultyKey = "DDR_DIFFICULTY_SHORT_CHALLENGE"
+ }
+
+ let difficultyName = NSLocalizedString(difficultyKey, comment: "")
+ return String(format: "%d %@", self.rating, difficultyName)
+ }
+ }
+
+ var difficultyColor: UIColor
+ {
+ get
+ {
+ var color: UIColor!
+
+ switch (self.difficulty) {
+ case .beginner:
+ color = #colorLiteral(red: 0.1411764771, green: 0.3960784376, blue: 0.5647059083, alpha: 1)
+ case .basic:
+ color = #colorLiteral(red: 0.7254902124, green: 0.4784313738, blue: 0.09803921729, alpha: 1)
+ case .difficult:
+ color = #colorLiteral(red: 0.4392156899, green: 0.01176470611, blue: 0.1921568662, alpha: 1)
+ case .expert:
+ color = #colorLiteral(red: 0.2745098174, green: 0.4862745106, blue: 0.1411764771, alpha: 1)
+ case .challenge:
+ color = #colorLiteral(red: 0.3647058904, green: 0.06666667014, blue: 0.9686274529, alpha: 1)
+ }
+
+ return color
+ }
+ }
+}
A Amalgamation/Source/Utility/DispatchExtensions.swift => Amalgamation/Source/Utility/DispatchExtensions.swift +16 -0
@@ 0,0 1,16 @@
+//
+// DispatchExtensions.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/21/18.
+//
+
+import Foundation
+
+extension DispatchTime
+{
+ static func secondsFromNow(_ seconds: Double) -> DispatchTime
+ {
+ return DispatchTime.now() + .milliseconds(Int(seconds * 1000.0))
+ }
+}
A Amalgamation/Source/Utility/ServerOperation.swift => Amalgamation/Source/Utility/ServerOperation.swift +57 -0
@@ 0,0 1,57 @@
+//
+// ServerOperation.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/22/18.
+//
+
+import Foundation
+
+class ServerOperation : Operation
+{
+ var baseURL: URL
+ var session: URLSession
+ internal(set) var error: Error?
+
+ init(baseURL: URL, session: URLSession)
+ {
+ self.baseURL = baseURL
+ self.session = session
+ }
+
+ func fetchData(_ url: URL) -> Data?
+ {
+ var fetchedData: Data?
+
+ let semaphore = Semaphore(value: 0)
+ let task = self.session.dataTask(with: url, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) -> Void in
+ fetchedData = data
+ self.error = error
+
+ semaphore.signal()
+ })
+ task.resume()
+
+ semaphore.wait()
+ return fetchedData
+ }
+
+ func fetchResponse(_ request: URLRequest) -> (Data?, URLResponse?)
+ {
+ var fetchedData: Data?
+ var fetchedResponse: URLResponse?
+
+ let semaphore = Semaphore(value: 0)
+ let task = self.session.dataTask(with: request, completionHandler: { (data: Data?, response: URLResponse?, error: Error?) in
+ fetchedResponse = response
+ fetchedData = data
+ self.error = error
+
+ semaphore.signal()
+ })
+ task.resume()
+
+ semaphore.wait()
+ return (fetchedData, fetchedResponse)
+ }
+}
A Amalgamation/Source/Utility/TournamentScreen.swift => Amalgamation/Source/Utility/TournamentScreen.swift +32 -0
@@ 0,0 1,32 @@
+//
+// TournamentScreen.swift
+// Amalgamation
+//
+// Created by Charles Magahern on 9/22/18.
+//
+
+import Foundation
+import UIKit
+
+protocol CustomTournamentScreen : Transitionable
+{
+ var shouldShowScreen: Bool { get }
+}
+
+struct TournamentScreenConfiguration
+{
+ var visibleMatchesRange: CountableClosedRange<Int>
+ var interstitialView: InterstitialView
+}
+
+struct TournamentScreen
+{
+ enum Content
+ {
+ case configuration(TournamentScreenConfiguration)
+ case viewController(UIViewController & CustomTournamentScreen)
+ }
+
+ var interstitialView: InterstitialView
+ var content: Content
+}
M Amalgamation/Source/Utility/Transitionable.swift => Amalgamation/Source/Utility/Transitionable.swift +12 -0
@@ 16,6 16,18 @@ struct Transition
protocol Transitionable : class
{
+ func prepareToTransitionToActive()
func transitionToActive(transition: Transition, completion: @escaping (Bool) -> Void)
+
+ func prepareToTransitionToInactive()
func transitionToInactive(transition: Transition, completion: @escaping (Bool) -> Void)
}
+
+extension Transitionable
+{
+ func prepareToTransitionToInactive()
+ {}
+
+ func prepareToTransitionToActive()
+ {}
+}
M Amalgamation/Source/View Controllers/TournamentViewController.swift => Amalgamation/Source/View Controllers/TournamentViewController.swift +152 -72
@@ 7,12 7,6 @@
import UIKit
-internal struct TournamentScreenConfiguration
-{
- var visibleMatchesRange: CountableClosedRange<Int>
- var interstitialView: InterstitialView
-}
-
class TournamentViewController : UIViewController, Transitionable
{
enum ScreenState
@@ 32,10 26,12 @@ class TournamentViewController : UIViewController, Transitionable
private let _tournamentBracketView: TournamentBracketView = TournamentBracketView()
private let _visualizationViewController: VisualizationViewController = VisualizationViewController()
+ private var _currentScreenCustomView: UIView?
+
private var _lastUpdatedTournamentData: Tournament?
private var _tournamentDataUpdateTimer: Timer?
- private var _screenConfigurations: [TournamentScreenConfiguration] = [] // loaded in viewDidLoad()
+ private var _screens: [TournamentScreen] = [] // loaded in viewDidLoad()
private var _currentScreenIndex: Int = -1
private var _currentScreenState: ScreenState = .screenVisible
private var _screenTimer: Timer?
@@ 131,26 127,43 @@ class TournamentViewController : UIViewController, Transitionable
view.addSubview(_competitivePlayersView)
view.addSubview(_tournamentBracketView)
view.addSubview(_visualizationViewController.view)
-
+
// initialize screen configurations
let mainBracketInterstitial = InterstitialView()
mainBracketInterstitial.text = NSLocalizedString("MAIN_BRACKET_INTERSTITIAL", comment: "")
mainBracketInterstitial.image = UIImage(named: "MainBracket")?.withRenderingMode(.alwaysTemplate)
+ let mainBracketScreen = TournamentScreen(
+ interstitialView: mainBracketInterstitial,
+ content: TournamentScreen.Content.configuration(TournamentScreenConfiguration(
+ visibleMatchesRange: 0...Int.max,
+ interstitialView: mainBracketInterstitial
+ ))
+ )
+
let losersBracketInterstitial = InterstitialView()
losersBracketInterstitial.text = NSLocalizedString("LOSERS_BRACKET_INTERSTITIAL", comment: "")
losersBracketInterstitial.image = UIImage(named: "LosersBracket")?.withRenderingMode(.alwaysTemplate)
- let mainBracketConfiguration = TournamentScreenConfiguration(
- visibleMatchesRange: 0...Int.max,
- interstitialView: mainBracketInterstitial
+ let losersBracketScreen = TournamentScreen(
+ interstitialView: losersBracketInterstitial,
+ content: TournamentScreen.Content.configuration(TournamentScreenConfiguration(
+ visibleMatchesRange: Int.min...0,
+ interstitialView: losersBracketInterstitial
+ ))
)
- let losersBracketConfiguration = TournamentScreenConfiguration(
- visibleMatchesRange: Int.min...0,
- interstitialView: losersBracketInterstitial
+
+ let ddrCardDrawInterstitial = InterstitialView()
+ ddrCardDrawInterstitial.text = NSLocalizedString("DDR_CARD_DRAW_INTERSTITIAL", comment: "")
+ ddrCardDrawInterstitial.image = UIImage(named: "CardDraw")?.withRenderingMode(.alwaysTemplate)
+
+ let ddrCardDrawViewController = DDRCardDrawViewController()
+ let ddrCardDrawScreen = TournamentScreen(
+ interstitialView: ddrCardDrawInterstitial,
+ content: TournamentScreen.Content.viewController(ddrCardDrawViewController)
)
- _screenConfigurations = [mainBracketConfiguration, losersBracketConfiguration]
+ _screens = [mainBracketScreen, losersBracketScreen, ddrCardDrawScreen]
_setCurrentScreenIndex(index: 0, animated: false)
// setup tap gesture recognizer for advancing state
@@ 261,6 274,9 @@ class TournamentViewController : UIViewController, Transitionable
)
_tournamentBracketView.frame = tournamentBracketViewFrame
+ // custom screen view container (if exists)
+ _currentScreenCustomView?.frame = tournamentBracketViewFrame
+
// interstitial view (if exists)
if let currentInterstitialView = _currentInterstitialView {
let interstitialBoundsSize = CGSize(
@@ 370,7 386,7 @@ class TournamentViewController : UIViewController, Transitionable
let interval = TournamentViewController._screenChangeTimerInterval
_screenTimer = Timer.scheduledTimer(withTimeInterval: interval, repeats: true, block: { [weak self] (_) in
- self?._advanceToNextTournamentScreen()
+ self?._advanceToNextAvailableTournamentScreen()
})
}
@@ 415,16 431,10 @@ class TournamentViewController : UIViewController, Transitionable
loadMatches: true) { (tournament: Tournament?, error: Error?) in
if let updatedTournament = tournament {
self?._lastUpdatedTournamentData = updatedTournament
+ } else if let error = error {
+ StandardErrorOutputStream.shared.write("ERROR: \(error.localizedDescription)\n")
} else {
- let alertTitle = NSLocalizedString("TOURNAMENT_LOAD_ERROR_TITLE", comment: "")
- let alertMessage = error?.localizedDescription ?? NSLocalizedString("UNKNOWN_ERROR", comment: "")
- let alert = UIAlertController(title: alertTitle, message: alertMessage, preferredStyle: .alert)
- alert.addAction(UIAlertAction(title: NSLocalizedString("OK", comment: ""), style: .default, handler: { (action: UIAlertAction) in
- self?.dismiss(animated: true, completion: nil)
- }))
-
- self?.present(alert, animated: true, completion: nil)
- self?.navigationController?.popViewController(animated: true) // pop to settings on error
+ StandardErrorOutputStream.shared.write("Unknown error occurred")
}
}
}
@@ 455,72 465,142 @@ class TournamentViewController : UIViewController, Transitionable
return countInRange
}
+ internal func _shouldShowScreen(_ screen: TournamentScreen) -> Bool
+ {
+ switch (screen.content) {
+ case .configuration(let configuration):
+ return (_tournamentMatchesCount(inRange: configuration.visibleMatchesRange) > 0)
+ case .viewController(let viewController):
+ return viewController.shouldShowScreen
+ }
+ }
+
+ internal func _transitionableForScreen(_ screen: TournamentScreen) -> Transitionable
+ {
+ switch (screen.content) {
+ case .configuration:
+ return self._tournamentBracketView
+ case .viewController(let viewController):
+ return viewController
+ }
+ }
+
+ internal func _transitionOutScreen(screen: TournamentScreen, completion: @escaping (Bool) -> Void)
+ {
+ let transitionable = _transitionableForScreen(screen)
+ let outTransition = Transition(
+ duration: Theme.mainTheme.transitionDuration,
+ delay: 0.0,
+ options: [.curveEaseIn, .beginFromCurrentState]
+ )
+
+ transitionable.prepareToTransitionToInactive()
+
+ _currentScreenState = .screenExiting
+ transitionable.transitionToInactive(transition: outTransition, completion: completion)
+ }
+
+ internal func _prepareScreen(_ screen: TournamentScreen)
+ {
+ switch (screen.content) {
+ case .configuration(let configuration):
+ self._currentScreenCustomView?.removeFromSuperview()
+ self._tournamentBracketView.isHidden = false
+
+ self._tournamentBracketView.visibleMatchesRange = configuration.visibleMatchesRange
+ self._tournamentBracketView.scrollToLatestVisibleRound(animated: false)
+
+ case .viewController(let viewController):
+ self._tournamentBracketView.isHidden = true
+
+ if viewController.parent == nil {
+ self.addChildViewController(viewController)
+ }
+
+ self._currentScreenCustomView = viewController.view
+ self.view.addSubview(viewController.view)
+ self.view.setNeedsLayout()
+ }
+ }
+
+ internal func _transitionInScreen(_ screen: TournamentScreen, setupCallback: @escaping () -> Void)
+ {
+ let transitionable = _transitionableForScreen(screen)
+
+ let inTransition = Transition(
+ duration: Theme.mainTheme.transitionDuration,
+ delay: 0.0,
+ options: [.curveEaseOut, .beginFromCurrentState]
+ )
+
+ let outTransition = Transition(
+ duration: Theme.mainTheme.transitionDuration,
+ delay: 0.0,
+ options: [.curveEaseIn, .beginFromCurrentState]
+ )
+
+ _currentInterstitialView = screen.interstitialView
+ _currentInterstitialView?.layoutIfNeeded()
+
+ _currentScreenState = .interstitialEntering
+ _currentInterstitialView?.transitionToActive(transition: inTransition, completion: { (_) in
+ // while the interstitial is visible, reload remote data
+ self._reloadTournamentDataUsingCache()
+ transitionable.prepareToTransitionToActive()
+
+ self._currentScreenState = .interstitialExiting
+ self._currentInterstitialView?.transitionToInactive(transition: outTransition, completion: { (_) in
+ self._currentInterstitialView = nil
+ setupCallback()
+
+ self._currentScreenState = .screenEntering
+ transitionable.transitionToActive(transition: inTransition, completion: { (_) in
+ self._currentScreenState = .screenVisible
+ })
+ })
+ })
+ }
+
internal func _setCurrentScreenIndex(index: Int, animated: Bool)
{
- guard let nextScreenConfiguration = _screenConfigurations[safe: index] else { return }
+ guard let nextScreen = _screens[safe: index] else { return }
+ let currentScreen = _screens[safe: _currentScreenIndex]
// don't show the next screen if there isn't any content visible
- if _tournamentMatchesCount(inRange: nextScreenConfiguration.visibleMatchesRange) == 0 {
+ if !_shouldShowScreen(nextScreen) {
return
}
let setupNextScreenBlock = {
- self._tournamentBracketView.visibleMatchesRange = nextScreenConfiguration.visibleMatchesRange
- self._tournamentBracketView.scrollToLatestVisibleRound(animated: false)
+ self._prepareScreen(nextScreen)
self._currentScreenIndex = index
}
if animated {
- let theme = Theme.mainTheme
- let outTransition = Transition(
- duration: theme.transitionDuration,
- delay: 0.0,
- options: [.curveEaseIn, .beginFromCurrentState]
- )
- let inTransition = Transition(
- duration: theme.transitionDuration,
- delay: 0.0,
- options: [.curveEaseOut, .beginFromCurrentState]
- )
-
- _currentScreenState = .screenExiting
- _tournamentBracketView.transitionToInactive(transition: outTransition) { (_) in
- self._currentInterstitialView = nextScreenConfiguration.interstitialView
- self._currentInterstitialView?.layoutIfNeeded()
-
- var interstitialTransitionIn = outTransition
- interstitialTransitionIn.duration = 0.5
-
- self._currentScreenState = .interstitialEntering
- self._currentInterstitialView?.transitionToActive(transition: inTransition, completion: { (_) in
- // while the interstitial is visible, reload the tournament data
- self._reloadTournamentDataUsingCache()
-
- var interstitialTransitionOut = interstitialTransitionIn
- interstitialTransitionOut.delay = 0.1
-
- self._currentScreenState = .interstitialExiting
- self._currentInterstitialView?.transitionToInactive(transition: interstitialTransitionOut, completion: { (_) in
- self._currentInterstitialView = nil
- setupNextScreenBlock()
-
- self._currentScreenState = .screenEntering
- self._tournamentBracketView.transitionToActive(transition: inTransition, completion: { (_) in
- self._currentScreenState = .screenVisible
- })
- })
- })
+ if let currentScreen = currentScreen {
+ self._transitionOutScreen(screen: currentScreen) { (_) in
+ self._transitionInScreen(nextScreen, setupCallback: setupNextScreenBlock)
+ }
+ } else {
+ self._transitionInScreen(nextScreen, setupCallback: setupNextScreenBlock)
}
} else {
setupNextScreenBlock()
}
}
- internal func _advanceToNextTournamentScreen()
+ internal func _advanceToNextAvailableTournamentScreen()
{
if _currentScreenState == .screenVisible {
- let nextIndex = (_currentScreenIndex + 1) % _screenConfigurations.count
- _setCurrentScreenIndex(index: nextIndex, animated: true)
+ var nextScreen: TournamentScreen?
+ var screenIndex = _currentScreenIndex
+
+ repeat {
+ screenIndex = (screenIndex + 1) % _screens.count
+ nextScreen = _screens[screenIndex]
+ } while (nextScreen != nil ? !_shouldShowScreen(nextScreen!) : false)
+
+ _setCurrentScreenIndex(index: screenIndex, animated: true)
}
}
@@ 528,7 608,7 @@ class TournamentViewController : UIViewController, Transitionable
{
if _currentScreenState == .screenVisible {
_resetTournamentScreenAdvanceTimer()
- _advanceToNextTournamentScreen()
+ _advanceToNextAvailableTournamentScreen()
}
}
}
M Amalgamation/Source/Views/InterstitialView.swift => Amalgamation/Source/Views/InterstitialView.swift +2 -4
@@ 134,13 134,11 @@ class InterstitialView : UIView, Transitionable
CATransaction.commit()
- let imageAnimDelayDispatch = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int(transition.delay * 1000.0))
- DispatchQueue.main.asyncAfter(deadline: imageAnimDelayDispatch) {
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.secondsFromNow(transition.delay)) {
self._imageView.layer.opacity = (active ? 1.0 : 0.0)
}
- let labelAnimDelayDispatch = DispatchTime.now() + DispatchTimeInterval.milliseconds(Int((transition.delay + labelAnimDelay) * 1000.0))
- DispatchQueue.main.asyncAfter(deadline: labelAnimDelayDispatch) {
+ DispatchQueue.main.asyncAfter(deadline: DispatchTime.secondsFromNow(transition.delay + labelAnimDelay)) {
self._label.layer.opacity = (active ? 1.0 : 0.0)
}
}
M Amalgamation/Source/Views/TournamentBracketView.swift => Amalgamation/Source/Views/TournamentBracketView.swift +1 -1
@@ 214,7 214,7 @@ class TournamentBracketView : UIView, UIScrollViewDelegate, Transitionable
_bracketScrollView.contentSize = maxSize
_bracketScrollView.contentInset = UIEdgeInsets(
top: 0.0,
- left: maxSize.width / 4.0,
+ left: bounds.size.width / 2.0 - maxSize.width / 2.0,
bottom: 0.0,
right: 0.0
)
M Amalgamation/Source/Views/TournamentStateView.swift => Amalgamation/Source/Views/TournamentStateView.swift +6 -2
@@ 222,7 222,9 @@ internal class TournamentStateIndicator : UIView
{
didSet
{
- _reloadAnimationState()
+ if oldValue != animating {
+ _reloadAnimationState()
+ }
}
}
@@ 230,7 232,9 @@ internal class TournamentStateIndicator : UIView
{
didSet
{
- _reloadAnimationState()
+ if oldValue != tournamentState {
+ _reloadAnimationState()
+ }
}
}
M Amalgamation/Supporting Files/Info.plist => Amalgamation/Supporting Files/Info.plist +6 -1
@@ 15,11 15,16 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
- <string>1.0</string>
+ <string>1.1</string>
<key>CFBundleVersion</key>
<string>1</string>
<key>LSRequiresIPhoneOS</key>
<true/>
+ <key>NSAppTransportSecurity</key>
+ <dict>
+ <key>NSAllowsArbitraryLoads</key>
+ <true/>
+ </dict>
<key>UIAppFonts</key>
<array>
<string>Karla-Italic.ttf</string>
M Amalgamation/Supporting Files/en.lproj/Localizable.strings => Amalgamation/Supporting Files/en.lproj/Localizable.strings +16 -0
@@ 32,11 32,15 @@
"ROUND_ORDINAL_FORMAT" = "Round %d";
"LOSERS_ROUND_ORDINAL_FORMAT" = "Losers Round %d";
+"MATCH_PLAYERS_VERSUS_FORMAT" = "%@ vs. %@";
+"MATCH_UNKNOWN_PLAYER_NAME" = "???";
+
"TOURNAMENT_COMPLETION_LABEL" = "Completion";
"COMPETITIVE_PLAYERS_LABEL" = "Competitive Players";
"MAIN_BRACKET_INTERSTITIAL" = "Main Bracket";
"LOSERS_BRACKET_INTERSTITIAL" = "Loser's Bracket";
+"DDR_CARD_DRAW_INTERSTITIAL" = "Current Match";
"TOURNAMENT_ID_PLACEHOLDER" = "Tournament ID";
"TOURNAMENT_ID_HELP_TEXT" = "The tournament identifier is the last part of the Challonge tournament URL. For example, if the URL is \"challonge.com/ddrsummer2017\", then the tournament ID is \"ddrsummer2017\".";
@@ 44,3 48,15 @@
"REMOVE_BUTTON_TEXT" = "Remove";
"TOURNAMENT_LOAD_ERROR_TITLE" = "Load Failed";
+
+"DDR_DIFFICULTY_BEGINNER" = "Beginner";
+"DDR_DIFFICULTY_BASIC" = "Basic";
+"DDR_DIFFICULTY_DIFFICULT" = "Difficult";
+"DDR_DIFFICULTY_EXPERT" = "Expert";
+"DDR_DIFFICULTY_CHALLENGE" = "Challenge";
+
+"DDR_DIFFICULTY_SHORT_BEGINNER" = "BEG";
+"DDR_DIFFICULTY_SHORT_BASIC" = "BAS";
+"DDR_DIFFICULTY_SHORT_DIFFICULTY" = "DIF";
+"DDR_DIFFICULTY_SHORT_EXPERT" = "EX";
+"DDR_DIFFICULTY_SHORT_CHALLENGE" = "CH";