Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion BitDream/BitDreamApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -282,7 +282,7 @@ private extension BitDreamApp {
.accentColor(themeManager.accentColor)
.environmentObject(themeManager)
.immediateTheme(manager: themeManager)
.frame(minWidth: 420, idealWidth: 460, maxWidth: 600, minHeight: 320, idealHeight: 360, maxHeight: 800)
.frame(minWidth: 420, idealWidth: 460, maxWidth: 600, minHeight: 320, idealHeight: 720, maxHeight: 800)
}
.windowResizability(.contentSize)
.modelContainer(persistenceController.container)
Expand Down
14 changes: 14 additions & 0 deletions BitDream/Formatting.swift
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,20 @@ func formatSpeed(_ bytesPerSecond: Int64) -> String {
return "\(base)/s"
}

func formatTransferRatio(uploadedBytes: Int64, downloadedBytes: Int64) -> String {
let ratio = downloadedBytes > 0 ? Double(uploadedBytes) / Double(downloadedBytes) : 0
return ratio.formatted(.number.precision(.fractionLength(2)))
}

func formatActiveDuration(_ seconds: Int64) -> String {
let formatter = DateComponentsFormatter()
formatter.allowedUnits = [.day, .hour, .minute, .second]
formatter.unitsStyle = .abbreviated
formatter.zeroFormattingBehavior = .dropLeading

return formatter.string(from: TimeInterval(max(0, seconds))) ?? "0s"
}

func formatCompactByteCount(_ bytes: Int64) -> CompactByteCountComponents {
let formatted = formattedByteCount(bytes)
var value = ""
Expand Down
101 changes: 62 additions & 39 deletions BitDream/Views/Shared/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func makeRatioSummarySnapshot(store: TransmissionStore, displayMode: RatioDispla
struct StatsHeaderView: View {
@EnvironmentObject private var themeManager: ThemeManager
@ObservedObject var store: TransmissionStore
let onShowStatistics: () -> Void
@AppStorage(UserDefaultsKeys.ratioDisplayMode) private var ratioDisplayModeRaw: String = AppDefaults.ratioDisplayMode.rawValue

// MARK: - Computed totals and ratio
Expand All @@ -196,6 +197,21 @@ struct StatsHeaderView: View {
ratioSummary.ratio
}

private var downloadSpeed: Int64 {
store.sessionStats?.downloadSpeed ?? 0
}

private var uploadSpeed: Int64 {
store.sessionStats?.uploadSpeed ?? 0
}

private var accessibilityValue: String {
let mode = ratioDisplayMode == .cumulative ? "Total ratio" : "Session ratio"
let ratio = overallRatio.formatted(.number.precision(.fractionLength(2)))
return "\(mode) \(ratio), download speed \(formatSpeed(downloadSpeed)), "
+ "upload speed \(formatSpeed(uploadSpeed))"
}

private var ratioTooltip: String {
let mode = ratioDisplayMode == .cumulative ? "Total Ratio" : "Session Ratio"
let uploaded = formatByteCount(ratioSummary.uploaded)
Expand All @@ -204,47 +220,54 @@ struct StatsHeaderView: View {
}

var body: some View {
HStack(spacing: 12) {
RatioChip(
ratio: overallRatio,
size: .compact,
helpText: ratioTooltip
)
.contextMenu {
Button(action: { ratioDisplayModeRaw = RatioDisplayMode.cumulative.rawValue }, label: {
HStack {
if ratioDisplayMode == .cumulative { Image(systemName: "checkmark") }
Text("Total Ratio")
}
})
Button(action: { ratioDisplayModeRaw = RatioDisplayMode.current.rawValue }, label: {
HStack {
if ratioDisplayMode == .current { Image(systemName: "checkmark") }
Text("Session Ratio")
}
})
}

Spacer()

HStack(spacing: 8) {
SpeedChip(
speed: store.sessionStats?.downloadSpeed ?? 0,
direction: .download,
style: .chip,
size: .compact
)

SpeedChip(
speed: store.sessionStats?.uploadSpeed ?? 0,
direction: .upload,
style: .chip,
size: .compact
Button(action: onShowStatistics) {
HStack(spacing: 12) {
RatioChip(
ratio: overallRatio,
size: .compact,
helpText: ratioTooltip
)
.contextMenu {
Button(action: { ratioDisplayModeRaw = RatioDisplayMode.cumulative.rawValue }, label: {
HStack {
if ratioDisplayMode == .cumulative { Image(systemName: "checkmark") }
Text("Total Ratio")
}
})
Button(action: { ratioDisplayModeRaw = RatioDisplayMode.current.rawValue }, label: {
HStack {
if ratioDisplayMode == .current { Image(systemName: "checkmark") }
Text("Session Ratio")
}
})
}

Spacer()

HStack(spacing: 8) {
SpeedChip(
speed: downloadSpeed,
direction: .download,
style: .chip,
size: .compact
)

SpeedChip(
speed: uploadSpeed,
direction: .upload,
style: .chip,
size: .compact
)
}
}
.padding(.horizontal)
.padding(.vertical, 8)
.contentShape(.rect)
}
.padding(.horizontal)
.padding(.vertical, 8)
.buttonStyle(.plain)
.accessibilityLabel("Statistics")
.accessibilityValue(accessibilityValue)
.accessibilityHint("Shows detailed session statistics")
}
}

Expand All @@ -257,7 +280,7 @@ struct StatsHeaderView: View {

#Preview("Statistics Header", traits: .sizeThatFitsLayout) {
PreviewContainer { environment in
StatsHeaderView(store: environment.store)
StatsHeaderView(store: environment.store, onShowStatistics: { })
.padding()
}
}
Expand Down
46 changes: 37 additions & 9 deletions BitDream/Views/iOS/iOSContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,24 @@ import SwiftUI
import Foundation

#if os(iOS)
enum iOSNavigationRoute: Hashable {
case torrent(Int)
}

struct iOSContentView: View {
let hosts: [Host]
@ObservedObject var store: TransmissionStore
private let userDefaults: UserDefaults

@State private var torrentPath: [Int] = []
@State private var navigationPath: [iOSNavigationRoute] = []

@State private var sortProperty: SortProperty
@State private var sortOrder: SortOrder
@State private var sidebarSelection: SidebarSelection = .allDreams
@State private var labelFilter = TorrentLabelFilter()
@AppStorage(UserDefaultsKeys.showContentTypeIcons) private var showContentTypeIcons = AppDefaults.showContentTypeIcons
@State private var searchText: String = ""
@State private var isStatisticsPresented = false
@State private var showPrefs: Bool = false
@State private var serverToEdit: Host?

Expand Down Expand Up @@ -53,7 +58,7 @@ struct iOSContentView: View {
}
.onChange(of: store.host?.serverID) { _, _ in
labelFilter.clear()
torrentPath.removeAll()
navigationPath.removeAll()
}
.onChange(of: store.torrents.map(\.id)) { _, torrentIDs in
reconcileNavigationPath(with: torrentIDs)
Expand All @@ -77,6 +82,12 @@ struct iOSContentView: View {
.sheet(isPresented: $store.showSettings, content: {
SettingsView(store: store)
})
.sheet(isPresented: $isStatisticsPresented) {
NavigationStack {
iOSStatisticsView(store: store)
}
.presentationDragIndicator(.visible)
}
}
}

Expand Down Expand Up @@ -119,11 +130,14 @@ private extension iOSContentView {
}

func mainContent(drawerWidth: CGFloat, progress: CGFloat) -> some View {
NavigationStack(path: $torrentPath) {
NavigationStack(path: $navigationPath) {
torrentListScreen
.navigationDestination(for: Int.self) { torrentID in
if let torrent = store.torrents.first(where: { $0.id == torrentID }) {
TorrentDetail(store: store, torrent: torrent)
.navigationDestination(for: iOSNavigationRoute.self) { route in
switch route {
case .torrent(let torrentID):
if let torrent = store.torrents.first(where: { $0.id == torrentID }) {
TorrentDetail(store: store, torrent: torrent)
}
}
}
}
Expand All @@ -144,7 +158,7 @@ private extension iOSContentView {
}
}
.overlay(alignment: .leading) {
if !isSidebarOpen && torrentPath.isEmpty {
if !isSidebarOpen && navigationPath.isEmpty {
Color.clear
.frame(width: 20)
.contentShape(.rect)
Expand Down Expand Up @@ -217,7 +231,7 @@ private extension iOSContentView {
private extension iOSContentView {
var torrentListScreen: some View {
VStack(spacing: 0) {
StatsHeaderView(store: store)
statisticsButton

Group {
if store.host != nil, store.connectionStatus != .connected {
Expand Down Expand Up @@ -250,6 +264,15 @@ private extension iOSContentView {
}
}

var statisticsButton: some View {
StatsHeaderView(
store: store,
onShowStatistics: {
isStatisticsPresented = true
}
)
}

var displayedTorrents: [Torrent] {
filterAndSortTorrents(
store.torrents,
Expand Down Expand Up @@ -389,7 +412,12 @@ private extension iOSContentView {

func reconcileNavigationPath(with torrentIDs: [Int]) {
let availableTorrentIDs = Set(torrentIDs)
torrentPath.removeAll { !availableTorrentIDs.contains($0) }
navigationPath.removeAll { route in
switch route {
case .torrent(let torrentID):
!availableTorrentIDs.contains(torrentID)
}
}
}

var hasActiveFilters: Bool {
Expand Down
Loading
Loading