Skip to content

Commit

Permalink
Add estimated download time
Browse files Browse the repository at this point in the history
  • Loading branch information
mallexxx committed Feb 26, 2024
1 parent 19c2656 commit 0ad5d2a
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 24 deletions.
1 change: 1 addition & 0 deletions DuckDuckGo/Common/Localizables/UserText.swift
Original file line number Diff line number Diff line change
Expand Up @@ -668,6 +668,7 @@ struct UserText {
static let downloadCanceled = NSLocalizedString("downloads.error.canceled", value: "Canceled", comment: "Short error description when downloaded file download was canceled")
static let downloadFailedToMoveFileToDownloads = NSLocalizedString("downloads.error.move.failed", value: "Could not move file to Downloads", comment: "Short error description when could not move downloaded file to the Downloads folder")
static let downloadFailed = NSLocalizedString("downloads.error.other", value: "Error", comment: "Short error description when Download failed")
static let downloadBytesLoadedFormat = NSLocalizedString("%@ of %@", comment: "Number of bytes out of total bytes downloaded (1Mb of 2Mb)")

static let cancelDownloadToolTip = NSLocalizedString("downloads.tooltip.cancel", value: "Cancel Download", comment: "Mouse-over tooltip for Cancel Download button")
static let restartDownloadToolTip = NSLocalizedString("downloads.tooltip.restart", value: "Restart Download", comment: "Mouse-over tooltip for Restart Download button")
Expand Down
44 changes: 37 additions & 7 deletions DuckDuckGo/FileDownload/Model/WebKitDownloadTask.swift
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ protocol WebKitDownloadTaskDelegate: AnyObject {
final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable {

static let downloadExtension = "duckload"
private enum Constants {
static let remainingDownloadTimeEstimationDelay: TimeInterval = 1
static let downloadSpeedSmoothingFactor = 0.1
}

let progress: Progress
let shouldPromptForLocation: Bool
Expand Down Expand Up @@ -72,7 +76,7 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable
private weak var delegate: WebKitDownloadTaskDelegate?

private let download: WebKitDownload
private var cancellables = Set<AnyCancellable>()
private var progressCancellable: AnyCancellable?

private var decideDestinationCompletionHandler: ((URL?) -> Void)?

Expand Down Expand Up @@ -114,12 +118,38 @@ final class WebKitDownloadTask: NSObject, ProgressReporting, @unchecked Sendable
private func start() {
self.progress.fileDownloadingSourceURL = download.originalRequest?.url
if let progress = (self.download as? ProgressReporting)?.progress {
progress.publisher(for: \.totalUnitCount)
.assign(to: \.totalUnitCount, onWeaklyHeld: self.progress)
.store(in: &self.cancellables)
progress.publisher(for: \.completedUnitCount)
.assign(to: \.completedUnitCount, onWeaklyHeld: self.progress)
.store(in: &self.cancellables)

var startTime: Date?
progressCancellable = progress.publisher(for: \.totalUnitCount)
.combineLatest(progress.publisher(for: \.completedUnitCount))
.sink { [weak progress=self.progress] total, completed in
guard let progress else { return }
if progress.totalUnitCount != total {
progress.totalUnitCount = total
}
progress.completedUnitCount = completed

if total > 0, completed > 0 {
guard let startTime else {
startTime = Date()
return
}
let elapsedTime = Date().timeIntervalSince(startTime)
guard elapsedTime > Constants.remainingDownloadTimeEstimationDelay else { return }

// Calculate instantaneous download speed
var throughput = Double(completed) / elapsedTime

// Calculate the moving average of download speed
if let oldThroughput = progress.throughput.map(Double.init) {
throughput = Constants.downloadSpeedSmoothingFactor * throughput + (1 - Constants.downloadSpeedSmoothingFactor) * oldThroughput
}
progress.throughput = Int(throughput)

// only update estimated time after initial delay
progress.estimatedTimeRemaining = Double(total - completed) / throughput
}
}
}
}

Expand Down
8 changes: 4 additions & 4 deletions DuckDuckGo/FileDownload/View/Downloads.storyboard
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@
<imageCell key="cell" refusesFirstResponder="YES" alignment="left" imageScaling="proportionallyDown" id="Iz7-Te-xl9"/>
</imageView>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="w5d-I6-kEZ">
<rect key="frame" x="43" y="32" width="96" height="16"/>
<rect key="frame" x="43" y="32" width="280" height="16"/>
<constraints>
<constraint firstAttribute="height" constant="16" id="e05-6Y-K6e"/>
</constraints>
Expand All @@ -134,7 +134,7 @@
</textFieldCell>
</textField>
<textField focusRingType="none" horizontalHuggingPriority="251" verticalHuggingPriority="750" horizontalCompressionResistancePriority="250" translatesAutoresizingMaskIntoConstraints="NO" id="ziJ-5p-mOT">
<rect key="frame" x="43" y="13" width="96" height="16"/>
<rect key="frame" x="43" y="13" width="280" height="16"/>
<textFieldCell key="cell" scrollable="YES" lineBreakMode="clipping" sendsActionOnEndEditing="YES" title="Table View Cell" usesSingleLineMode="YES" id="P5k-0F-CZE">
<font key="font" usesAppearanceFont="YES"/>
<color key="textColor" name="secondaryLabelColor" catalog="System" colorSpace="catalog"/>
Expand Down Expand Up @@ -247,12 +247,12 @@
<constraint firstItem="Fue-CV-Ukm" firstAttribute="leading" secondItem="0Hy-P1-z0d" secondAttribute="leading" id="QBC-Js-e0p"/>
<constraint firstItem="Fue-CV-Ukm" firstAttribute="top" secondItem="ziJ-5p-mOT" secondAttribute="bottom" constant="12" id="QwU-JU-idK"/>
<constraint firstItem="hpj-1Y-eDv" firstAttribute="centerY" secondItem="0Hy-P1-z0d" secondAttribute="centerY" id="T7a-0E-ouJ"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="w5d-I6-kEZ" secondAttribute="trailing" constant="8" id="U1h-uy-ann"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="leading" secondItem="w5d-I6-kEZ" secondAttribute="trailing" constant="8" id="U1h-uy-ann"/>
<constraint firstItem="Ejd-n2-PCA" firstAttribute="centerY" secondItem="foT-7L-XqL" secondAttribute="centerY" id="ZL1-MJ-Czx"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="centerY" secondItem="BUB-Ea-xr5" secondAttribute="centerY" id="enG-OL-lav"/>
<constraint firstItem="w5d-I6-kEZ" firstAttribute="leading" secondItem="hpj-1Y-eDv" secondAttribute="trailing" constant="6" id="gSS-n9-QXX"/>
<constraint firstAttribute="trailing" secondItem="Fue-CV-Ukm" secondAttribute="trailing" id="oZ0-bh-Iaf"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="leading" relation="greaterThanOrEqual" secondItem="ziJ-5p-mOT" secondAttribute="trailing" constant="8" id="rp5-hJ-Ccg"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="leading" secondItem="ziJ-5p-mOT" secondAttribute="trailing" constant="8" id="rp5-hJ-Ccg"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="centerX" secondItem="Ejd-n2-PCA" secondAttribute="centerX" id="uRp-Pm-Vkf"/>
<constraint firstItem="hpj-1Y-eDv" firstAttribute="leading" secondItem="0Hy-P1-z0d" secondAttribute="leading" constant="7" id="vnq-0C-72q"/>
<constraint firstItem="CEa-RP-JgA" firstAttribute="centerY" secondItem="Ejd-n2-PCA" secondAttribute="centerY" id="xeM-DX-IDw"/>
Expand Down
48 changes: 36 additions & 12 deletions DuckDuckGo/FileDownload/View/DownloadsCellView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,22 @@ final class DownloadsCellView: NSTableCellView {
private var progressCancellable: AnyCancellable?

private static let byteFormatter = ByteCountFormatter()
private static let estimatedMinutesRemainingFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.hour, .minute]
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = true
formatter.includesTimeRemainingPhrase = true
return formatter
}()
private static let estimatedSecondsRemainingFormatter: DateComponentsFormatter = {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.second]
formatter.unitsStyle = .full
formatter.includesApproximationPhrase = true
formatter.includesTimeRemainingPhrase = true
return formatter
}()

var isSelected: Bool = false {
didSet {
Expand Down Expand Up @@ -140,26 +156,34 @@ final class DownloadsCellView: NSTableCellView {
private var onButtonMouseOverChange: ((Bool) -> Void)?

private func updateDetails(with progress: Progress) {
self.detailLabel.toolTip = progress.localizedAdditionalDescription ?? ""

var details: String
if cancelButton.isMouseOver {
details = UserText.cancelDownloadToolTip
} else {
details = progress.localizedAdditionalDescription ?? ""
if details.isEmpty {
if progress.fractionCompleted == 0 {
details = UserText.downloadStarting
} else if progress.fractionCompleted == 1.0 {
details = UserText.downloadFinishing
} else {
assertionFailure("Unexpected empty description")
details = "Downloading…"
}
if progress.fractionCompleted == 0 {
details = UserText.downloadStarting
} else if progress.fractionCompleted == 1.0 {
details = UserText.downloadFinishing
} else if progress.totalUnitCount > 0 {
let completed = Self.byteFormatter.string(fromByteCount: progress.completedUnitCount)
let total = Self.byteFormatter.string(fromByteCount: progress.totalUnitCount)
details = String(format: UserText.downloadBytesLoadedFormat, completed, total)
} else {
details = Self.byteFormatter.string(fromByteCount: progress.completedUnitCount)
}

if let estimatedTimeRemaining = progress.estimatedTimeRemaining,
let estimatedTimeStr = (estimatedTimeRemaining < 60 ? Self.estimatedSecondsRemainingFormatter : Self.estimatedMinutesRemainingFormatter)
.string(from: estimatedTimeRemaining) {

details += "" + estimatedTimeStr
}

self.detailLabel.toolTip = progress.localizedDescription
self.detailLabel.stringValue = details
}

self.detailLabel.stringValue = details
}

private func subscribe(to progress: Progress) {
Expand Down
2 changes: 1 addition & 1 deletion DuckDuckGo/FileDownload/View/DownloadsViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ final class DownloadsViewController: NSViewController {
return
}
self.dismiss()
NSWorkspace.shared.selectFile(nil, inFileViewerRootedAtPath: url.path)
NSWorkspace.shared.selectFile(itemToSelect?.path, inFileViewerRootedAtPath: url.path)

Check failure on line 151 in DuckDuckGo/FileDownload/View/DownloadsViewController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Browser)

cannot find 'itemToSelect' in scope

Check failure on line 151 in DuckDuckGo/FileDownload/View/DownloadsViewController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Browser)

cannot find 'itemToSelect' in scope

Check failure on line 151 in DuckDuckGo/FileDownload/View/DownloadsViewController.swift

View workflow job for this annotation

GitHub Actions / Make Release Build (DuckDuckGo Privacy Pro)

cannot find 'itemToSelect' in scope

Check failure on line 151 in DuckDuckGo/FileDownload/View/DownloadsViewController.swift

View workflow job for this annotation

GitHub Actions / Test (Non-Sandbox)

cannot find 'itemToSelect' in scope
}

@IBAction func clearDownloadsAction(_ sender: Any) {
Expand Down
11 changes: 11 additions & 0 deletions DuckDuckGo/Localizable.xcstrings
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@
"%@ does not support storing passwords" : {
"comment" : "Data Import disabled checkbox message about a browser (%@) not supporting storing passwords"
},
"%@ of %@" : {
"comment" : "Number of bytes out of total bytes downloaded (1Mb of 2Mb)",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "%1$@ of %2$@"
}
}
}
},
"%lld" : {

},
Expand Down

0 comments on commit 0ad5d2a

Please sign in to comment.