From b7225b8bbca4bd007166bdb8cc34820307d53cef Mon Sep 17 00:00:00 2001 From: Ilya Laktyushin Date: Sat, 17 Nov 2018 18:24:30 +0400 Subject: [PATCH] Various UI fixes --- TelegramUI.xcodeproj/project.pbxproj | 4 ++ TelegramUI/ChatMediaInputNode.swift | 43 +++++++++++++------ .../ChatMessageInteractiveFileNode.swift | 3 +- .../ChatMessageInteractiveMediaNode.swift | 13 +++--- .../DataPrivacySettingsController.swift | 20 ++++----- TelegramUI/DebugController.swift | 35 +++++++++------ TelegramUI/GridMessageItem.swift | 2 +- TelegramUI/ID3Artwork.m | 11 +++-- TelegramUI/InstantPageNavigationBar.swift | 2 +- TelegramUI/MultiplexedVideoNode.swift | 10 +---- TelegramUI/PhotoResources.swift | 2 +- TelegramUI/StringForDuration.swift | 16 +++++++ 12 files changed, 103 insertions(+), 58 deletions(-) create mode 100644 TelegramUI/StringForDuration.swift diff --git a/TelegramUI.xcodeproj/project.pbxproj b/TelegramUI.xcodeproj/project.pbxproj index 747f6d95b7..e1b4d79603 100644 --- a/TelegramUI.xcodeproj/project.pbxproj +++ b/TelegramUI.xcodeproj/project.pbxproj @@ -59,6 +59,7 @@ 09C500242142BA6400EF253E /* ItemListWebsiteItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */; }; 09C9EA33219F79F600E90146 /* ID3Artwork.m in Sources */ = {isa = PBXBuildFile; fileRef = 09C9EA31219F79F500E90146 /* ID3Artwork.m */; }; 09C9EA34219F79F600E90146 /* ID3Artwork.h in Headers */ = {isa = PBXBuildFile; fileRef = 09C9EA32219F79F600E90146 /* ID3Artwork.h */; }; + 09C9EA3821A044B500E90146 /* StringForDuration.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09C9EA3721A044B500E90146 /* StringForDuration.swift */; }; 09D304152173C0E900C00567 /* WatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304142173C0E900C00567 /* WatchManager.swift */; }; 09D304182173C15700C00567 /* WatchSettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09D304172173C15700C00567 /* WatchSettingsController.swift */; }; 09FE756D2153F5F900A3120F /* CallRouteActionSheetItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */; }; @@ -1118,6 +1119,7 @@ 09C500232142BA6400EF253E /* ItemListWebsiteItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ItemListWebsiteItem.swift; sourceTree = ""; }; 09C9EA31219F79F500E90146 /* ID3Artwork.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ID3Artwork.m; sourceTree = ""; }; 09C9EA32219F79F600E90146 /* ID3Artwork.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ID3Artwork.h; sourceTree = ""; }; + 09C9EA3721A044B500E90146 /* StringForDuration.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = StringForDuration.swift; sourceTree = ""; }; 09D304142173C0E900C00567 /* WatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchManager.swift; sourceTree = ""; }; 09D304172173C15700C00567 /* WatchSettingsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchSettingsController.swift; sourceTree = ""; }; 09FE756C2153F5F900A3120F /* CallRouteActionSheetItem.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CallRouteActionSheetItem.swift; sourceTree = ""; }; @@ -4491,6 +4493,7 @@ 0902838C2194AEB90067EFBD /* ImageTransparency.swift */, 09C9EA32219F79F600E90146 /* ID3Artwork.h */, 09C9EA31219F79F500E90146 /* ID3Artwork.m */, + 09C9EA3721A044B500E90146 /* StringForDuration.swift */, ); name = Utils; sourceTree = ""; @@ -5085,6 +5088,7 @@ D0EC6D221EB9F58800EBF1C3 /* PhotoResources.swift in Sources */, D048EA871F4F296400188713 /* InstantPageSettingsFontSizeItemNode.swift in Sources */, D0EC6D231EB9F58800EBF1C3 /* StickerResources.swift in Sources */, + 09C9EA3821A044B500E90146 /* StringForDuration.swift in Sources */, D0EC6D241EB9F58800EBF1C3 /* CachedResourceRepresentations.swift in Sources */, D01BAA201ECC9A2500295217 /* CallListNodeLocation.swift in Sources */, D0EC6D251EB9F58800EBF1C3 /* FetchCachedRepresentations.swift in Sources */, diff --git a/TelegramUI/ChatMediaInputNode.swift b/TelegramUI/ChatMediaInputNode.swift index 7da6138763..50b32b48f8 100644 --- a/TelegramUI/ChatMediaInputNode.swift +++ b/TelegramUI/ChatMediaInputNode.swift @@ -1033,6 +1033,7 @@ final class ChatMediaInputNode: ChatInputNode { private func updateAppearanceTransition(transition: ContainedViewLayoutTransition) { var value: CGFloat = 1.0 - abs(self.currentCollectionListPanelOffset() / 41.0) value = min(1.0, max(0.0, value)) + self.inputNodeInteraction.appearanceTransition = max(0.1, value) transition.updateAlpha(node: self.listView, alpha: value) self.listView.forEachItemNode { itemNode in @@ -1046,6 +1047,8 @@ final class ChatMediaInputNode: ChatInputNode { itemNode.updateAppearanceTransition(transition: transition) } else if let itemNode = itemNode as? ChatMediaInputPeerSpecificItemNode { itemNode.updateAppearanceTransition(transition: transition) + } else if let itemNode = itemNode as? ChatMediaInputSettingsItemNode { + itemNode.updateAppearanceTransition(transition: transition) } } } @@ -1071,6 +1074,10 @@ final class ChatMediaInputNode: ChatInputNode { panelHeight = maximumHeight displaySearch = true } + self.stickerPane.collectionListPanelOffset = 0.0 + self.gifPane.collectionListPanelOffset = 0.0 + self.trendingPane.collectionListPanelOffset = 0.0 + self.updateAppearanceTransition(transition: transition) } else { panelHeight = standardInputHeight } @@ -1378,20 +1385,32 @@ final class ChatMediaInputNode: ChatInputNode { } } - private func updatePaneDidScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState, transition: ContainedViewLayoutTransition) { - var computedAbsoluteOffset: CGFloat - if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { - computedAbsoluteOffset = 0.0 - } else { - computedAbsoluteOffset = pane.collectionListPanelOffset + state.relativeChange + private var isExpanded: Bool { + var isExpanded: Bool = false + if let validLayout = self.validLayout, case let .media(_, maybeExpanded) = validLayout.8.inputMode, maybeExpanded != nil { + isExpanded = true } - computedAbsoluteOffset = max(-41.0, min(computedAbsoluteOffset, 0.0)) - pane.collectionListPanelOffset = computedAbsoluteOffset - if transition.isAnimated { - if pane.collectionListPanelOffset < -41.0 / 2.0 { - pane.collectionListPanelOffset = -41.0 + return isExpanded + } + + private func updatePaneDidScroll(pane: ChatMediaInputPane, state: ChatMediaInputPaneScrollState, transition: ContainedViewLayoutTransition) { + if self.isExpanded { + pane.collectionListPanelOffset = 0.0 + } else { + var computedAbsoluteOffset: CGFloat + if let absoluteOffset = state.absoluteOffset, absoluteOffset >= 0.0 { + computedAbsoluteOffset = 0.0 } else { - pane.collectionListPanelOffset = 0.0 + computedAbsoluteOffset = pane.collectionListPanelOffset + state.relativeChange + } + computedAbsoluteOffset = max(-41.0, min(computedAbsoluteOffset, 0.0)) + pane.collectionListPanelOffset = computedAbsoluteOffset + if transition.isAnimated { + if pane.collectionListPanelOffset < -41.0 / 2.0 { + pane.collectionListPanelOffset = -41.0 + } else { + pane.collectionListPanelOffset = 0.0 + } } } diff --git a/TelegramUI/ChatMessageInteractiveFileNode.swift b/TelegramUI/ChatMessageInteractiveFileNode.swift index 0abb810f11..0692445ef8 100644 --- a/TelegramUI/ChatMessageInteractiveFileNode.swift +++ b/TelegramUI/ChatMessageInteractiveFileNode.swift @@ -272,7 +272,8 @@ final class ChatMessageInteractiveFileNode: ASDisplayNode { audioDuration = Int32(duration) if voice { isVoice = true - candidateDescriptionString = NSAttributedString(string: String(format: "%d:%02d", duration / 60, duration % 60), font: durationFont, textColor:incoming ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) + let durationString = stringForDuration(audioDuration) + candidateDescriptionString = NSAttributedString(string: durationString, font: durationFont, textColor:incoming ? bubbleTheme.incomingFileDurationColor : bubbleTheme.outgoingFileDurationColor) if let waveform = waveform { waveform.withDataNoCopy { data in audioWaveform = AudioWaveform(bitstream: data, bitsPerSample: 5) diff --git a/TelegramUI/ChatMessageInteractiveMediaNode.swift b/TelegramUI/ChatMessageInteractiveMediaNode.swift index 886d74530c..1e9858a7fb 100644 --- a/TelegramUI/ChatMessageInteractiveMediaNode.swift +++ b/TelegramUI/ChatMessageInteractiveMediaNode.swift @@ -595,7 +595,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if let size = file.size { if let duration = file.duration, !message.flags.contains(.Unsent) { if isMediaStreamable(message: message, media: file) { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + let durationString = stringForDuration(duration) let sizeString = "\(dataSizeString(Int(Float(size) * progress), forceDecimal: true)) / \(dataSizeString(size, forceDecimal: true))" badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: sizeString) mediaDownloadState = .fetching(progress: progress) @@ -618,7 +618,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } } else { if let duration = file.duration, !file.isAnimated { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + let durationString = stringForDuration(duration) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) } } @@ -649,7 +649,7 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { } if case .constrained = sizeCalculation { if let file = media as? TelegramMediaFile, let duration = file.duration, !file.isAnimated { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + let durationString = stringForDuration(duration) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) } } @@ -660,16 +660,15 @@ final class ChatMessageInteractiveMediaNode: ASDisplayNode { if isMediaStreamable(message: message, media: file) { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) - + let durationString = stringForDuration(duration) badgeContent = .mediaDownload(backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, duration: durationString, size: dataSizeString(file.size ?? 0)) mediaDownloadState = .remote } else { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + let durationString = stringForDuration(duration) badgeContent = .text(inset: 0.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) } } else { - let durationString = String(format: "%d:%02d", duration / 60, duration % 60) + let durationString = stringForDuration(duration) if isMediaStreamable(message: message, media: file) { state = .play(bubbleTheme.mediaOverlayControlForegroundColor) badgeContent = .text(inset: 16.0, backgroundColor: bubbleTheme.mediaDateAndStatusFillColor, foregroundColor: bubbleTheme.mediaDateAndStatusTextColor, shape: .round, text: NSAttributedString(string: durationString)) diff --git a/TelegramUI/DataPrivacySettingsController.swift b/TelegramUI/DataPrivacySettingsController.swift index 3a48243260..c92678f722 100644 --- a/TelegramUI/DataPrivacySettingsController.swift +++ b/TelegramUI/DataPrivacySettingsController.swift @@ -400,16 +400,16 @@ public func dataPrivacyController(account: Account) -> ViewController { return settings }) - actionsDisposable.add((deleteAllContacts(postbox: account.postbox, network: account.network) - |> deliverOnMainQueue).start(completed: { - updateState { state in - var state = state - state.deletingContacts = false - return state - } - let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } - presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success)) - })) + actionsDisposable.add(((deleteAllContacts(postbox: account.postbox, network: account.network) |> then(resetSavedContacts(network: account.network))) + |> deliverOnMainQueue).start(completed: { + updateState { state in + var state = state + state.deletingContacts = false + return state + } + let presentationData = account.telegramApplicationContext.currentPresentationData.with { $0 } + presentControllerImpl?(OverlayStatusController(theme: presentationData.theme, strings: presentationData.strings, type: .success)) + })) }), TextAlertAction(type: .defaultAction, title: presentationData.strings.Common_Cancel, action: {})])) } }, updateSyncContacts: { value in diff --git a/TelegramUI/DebugController.swift b/TelegramUI/DebugController.swift index 6b02453031..22c2520c3f 100644 --- a/TelegramUI/DebugController.swift +++ b/TelegramUI/DebugController.swift @@ -21,7 +21,7 @@ private final class DebugControllerArguments { private enum DebugControllerSection: Int32 { case logs - case payments + case removal case logging case experiments case info @@ -31,6 +31,7 @@ private enum DebugControllerEntry: ItemListNodeEntry { case sendLogs(PresentationTheme) case sendOneLog(PresentationTheme) case accounts(PresentationTheme) + case resetServerContacts(PresentationTheme) case clearPaymentData(PresentationTheme) case logToFile(PresentationTheme, Bool) case logToConsole(PresentationTheme, Bool) @@ -47,8 +48,8 @@ private enum DebugControllerEntry: ItemListNodeEntry { return DebugControllerSection.logs.rawValue case .accounts: return DebugControllerSection.logs.rawValue - case .clearPaymentData: - return DebugControllerSection.payments.rawValue + case .resetServerContacts, .clearPaymentData: + return DebugControllerSection.removal.rawValue case .logToFile, .logToConsole, .redactSensitiveData: return DebugControllerSection.logging.rawValue case .enableRaiseToSpeak, .keepChatNavigationStack: @@ -68,24 +69,26 @@ private enum DebugControllerEntry: ItemListNodeEntry { return 1 case .accounts: return 2 - case .clearPaymentData: + case .resetServerContacts: return 3 - case .logToFile: + case .clearPaymentData: return 4 - case .logToConsole: + case .logToFile: return 5 - case .redactSensitiveData: + case .logToConsole: return 6 - case .enableRaiseToSpeak: + case .redactSensitiveData: return 7 - case .keepChatNavigationStack: + case .enableRaiseToSpeak: return 8 - case .clearTips: + case .keepChatNavigationStack: return 9 - case .reimport: + case .clearTips: return 10 - case .versionInfo: + case .reimport: return 11 + case .versionInfo: + return 12 } } @@ -141,8 +144,12 @@ private enum DebugControllerEntry: ItemListNodeEntry { return ItemListDisclosureItem(theme: theme, title: "Accounts", label: "", sectionId: self.section, style: .blocks, action: { arguments.pushController(debugAccountsController(account: arguments.account, accountManager: arguments.accountManager)) }) + case let .resetServerContacts(theme): + return ItemListActionItem(theme: theme, title: "Reset Server Contacts", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { + let _ = resetSavedContacts(network: arguments.account.network).start() + }) case let .clearPaymentData(theme): - return ItemListDisclosureItem(theme: theme, title: "Clear Payment Password", label: "", sectionId: self.section, style: .blocks, action: { + return ItemListActionItem(theme: theme, title: "Clear Payment Password", kind: .generic, alignment: .natural, sectionId: self.section, style: .blocks, action: { let _ = cacheTwoStepPasswordToken(postbox: arguments.account.postbox, token: nil).start() }) case let .logToFile(theme, value): @@ -214,6 +221,8 @@ private func debugControllerEntries(presentationData: PresentationData, loggingS entries.append(.sendLogs(presentationData.theme)) entries.append(.sendOneLog(presentationData.theme)) entries.append(.accounts(presentationData.theme)) + + entries.append(.resetServerContacts(presentationData.theme)) entries.append(.clearPaymentData(presentationData.theme)) entries.append(.logToFile(presentationData.theme, loggingSettings.logToFile)) diff --git a/TelegramUI/GridMessageItem.swift b/TelegramUI/GridMessageItem.swift index 10d0284e73..a1d1f7e10f 100644 --- a/TelegramUI/GridMessageItem.swift +++ b/TelegramUI/GridMessageItem.swift @@ -237,7 +237,7 @@ final class GridMessageItemNode: GridItemNode { self.imageNode.setSignal(mediaGridMessageVideo(postbox: account.postbox, videoReference: .message(message: MessageReference(item.message), media: file))) if let duration = file.duration { - videoAccessoryNode.setup(String(format: "%d:%02d", duration / 60, duration % 60)) + videoAccessoryNode.setup(stringForDuration(duration)) videoAccessoryNode.isHidden = false } else { videoAccessoryNode.isHidden = true diff --git a/TelegramUI/ID3Artwork.m b/TelegramUI/ID3Artwork.m index b2340f40c4..f91baa38a3 100644 --- a/TelegramUI/ID3Artwork.m +++ b/TelegramUI/ID3Artwork.m @@ -54,7 +54,6 @@ NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { } const uint8_t *bytes = data.bytes; - if (!(memcmp(bytes, ID3v2, 5) == 0 || memcmp(bytes, ID3v3, 5) == 0)) { return nil; } @@ -67,7 +66,7 @@ NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { uint32_t b4 = size & 0x0000007F; size = b1 + b2 + b3 + b4; - const uint8_t *ptr = data.bytes + ID3TagOffset; + const uint8_t *ptr = bytes + ID3TagOffset; uint32_t pos = 0; while (pos < size) { @@ -97,8 +96,12 @@ NSData * _Nullable albumArtworkData(NSData * _Nonnull data) { uint8_t previousByte = 0xff; uint32_t skippedBytes = 0; - for (NSUInteger i = 0; i < frameSize - imageOffset + skippedBytes; i++) { - uint8_t byte = (uint8_t)ptr[imageOffset + i]; + for (uint32_t i = 0; i < frameSize - imageOffset + skippedBytes; i++) { + uint32_t offset = imageOffset + i; + if (ID3TagOffset + pos + offset > data.length) { + return nil; + } + uint8_t byte = (uint8_t)ptr[offset]; if (byte == 0x00 && previousByte == 0xff) { skippedBytes++; } else { diff --git a/TelegramUI/InstantPageNavigationBar.swift b/TelegramUI/InstantPageNavigationBar.swift index 0335efcfc1..218f8a85b4 100644 --- a/TelegramUI/InstantPageNavigationBar.swift +++ b/TelegramUI/InstantPageNavigationBar.swift @@ -163,7 +163,7 @@ final class InstantPageNavigationBar: ASDisplayNode { if let title = title { self.titleNode.transform = CATransform3DIdentity self.titleNode.attributedText = NSAttributedString(string: title, font: Font.semibold(17.0), textColor: .white, paragraphAlignment: .center) - let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 44.0 - 88.0, height: size.height)) + let titleSize = self.titleNode.measure(CGSize(width: size.width - leftInset - rightInset - 30.0 - 88.0, height: size.height)) self.titleNode.frame = CGRect(origin: CGPoint(x: (size.width - titleSize.width) / 2.0, y: size.height - 30.0), size: titleSize) } } diff --git a/TelegramUI/MultiplexedVideoNode.swift b/TelegramUI/MultiplexedVideoNode.swift index 1e85ddd627..d8cc3eb1d5 100644 --- a/TelegramUI/MultiplexedVideoNode.swift +++ b/TelegramUI/MultiplexedVideoNode.swift @@ -220,9 +220,8 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { self.visibleThumbnailLayers[item.fileReference.media.fileId] = thumbnailLayer } - let progressFrame = CGRect(origin: CGPoint(x: item.frame.midX - 37 / 2, y: item.frame.midY - 37 / 2), size: CGSize(width: 37, height: 37)) - - + let progessSize = CGSize(width: 24.0, height: 24.0) + let progressFrame = CGRect(origin: CGPoint(x: item.frame.midX - progessSize.width / 2.0, y: item.frame.midY - progessSize.height / 2.0), size: progessSize) let updatedStatusSignal = account.postbox.mediaBox.resourceStatus(item.fileReference.media.resource) @@ -240,10 +239,6 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { let state: RadialStatusNodeState - - - - switch status { case let .Fetching(_, progress): state = .progress(color: .white, lineWidth: nil, value: CGFloat(max(progress, 0.2)), cancelEnabled: false) @@ -278,7 +273,6 @@ final class MultiplexedVideoNode: UIScrollView, UIScrollViewDelegate { } })) - if item.frame.maxY < minVisibleY { continue; } diff --git a/TelegramUI/PhotoResources.swift b/TelegramUI/PhotoResources.swift index 1f3a49feaa..03d5faf381 100644 --- a/TelegramUI/PhotoResources.swift +++ b/TelegramUI/PhotoResources.swift @@ -2522,7 +2522,7 @@ func playerAlbumArt(postbox: Postbox, fileReference: FileMediaReference?, albumA var fileArtworkData: Signal = .single(nil) if let fileReference = fileReference, let size = fileReference.media.resource.size { fileArtworkData = fileArtworkData - |> then(postbox.mediaBox.resourceData(fileReference.media.resource, size: size, in: 0 ..< min(size, 1024 * 256)) + |> then(postbox.mediaBox.resourceData(fileReference.media.resource, size: size, in: 0 ..< min(size, 1024 * 384)) |> mapToSignal { data -> Signal in return .single(albumArtworkData(data)) }) diff --git a/TelegramUI/StringForDuration.swift b/TelegramUI/StringForDuration.swift new file mode 100644 index 0000000000..1a93b32a2e --- /dev/null +++ b/TelegramUI/StringForDuration.swift @@ -0,0 +1,16 @@ +import Foundation + +func stringForDuration(_ duration: Int32) -> String { + let hours = duration / 3600 + let minutes = duration / 60 % 60 + let seconds = duration % 60 + let durationString: String + if hours > 0 { + durationString = String(format: "%d:%02d:%02d", hours, minutes, seconds) + } else { + durationString = String(format: "%d:%02d", minutes, seconds) + } + return durationString +} + +