Yattee v2 rewrite

This commit is contained in:
Arkadiusz Fal
2026-02-08 18:31:16 +01:00
parent 20d0cfc0c7
commit 05f921d605
1043 changed files with 163875 additions and 68430 deletions

View File

@@ -0,0 +1,224 @@
//
// DownloadSettings.swift
// Yattee
//
// Local-only settings for downloads sorting and grouping.
// These settings are NOT synced to iCloud.
//
import Foundation
/// Sort options for completed downloads.
enum DownloadSortOption: String, CaseIterable, Codable {
case name
case downloadDate
case fileSize
var displayName: String {
switch self {
case .name:
return String(localized: "downloads.sort.name")
case .downloadDate:
return String(localized: "downloads.sort.downloadDate")
case .fileSize:
return String(localized: "downloads.sort.fileSize")
}
}
var systemImage: String {
switch self {
case .name:
return "textformat"
case .downloadDate:
return "calendar"
case .fileSize:
return "internaldrive"
}
}
}
/// Sort direction.
enum SortDirection: String, CaseIterable, Codable {
case ascending
case descending
var systemImage: String {
switch self {
case .ascending:
return "arrow.up"
case .descending:
return "arrow.down"
}
}
mutating func toggle() {
self = self == .ascending ? .descending : .ascending
}
}
/// Manages download view settings locally (not synced to iCloud).
@MainActor
@Observable
final class DownloadSettings {
// MARK: - Storage Keys
private enum Keys {
static let sortOption = "downloads.sortOption"
static let sortDirection = "downloads.sortDirection"
static let groupByChannel = "downloads.groupByChannel"
static let allowCellularDownloads = "downloads.allowCellularDownloads"
static let preferredQuality = "downloads.preferredQuality"
static let includeSubtitlesInAutoDownload = "downloads.includeSubtitlesInAutoDownload"
static let maxConcurrentDownloads = "downloads.maxConcurrentDownloads"
}
// MARK: - Storage
private let defaults = UserDefaults.standard
// MARK: - Cached Values
private var _sortOption: DownloadSortOption?
private var _sortDirection: SortDirection?
private var _groupByChannel: Bool?
private var _allowCellularDownloads: Bool?
private var _preferredDownloadQuality: DownloadQuality?
private var _includeSubtitlesInAutoDownload: Bool?
private var _maxConcurrentDownloads: Int?
// MARK: - Properties
/// The current sort option for completed downloads.
var sortOption: DownloadSortOption {
get {
if let cached = _sortOption { return cached }
guard let rawValue = defaults.string(forKey: Keys.sortOption),
let option = DownloadSortOption(rawValue: rawValue) else {
return .downloadDate
}
return option
}
set {
_sortOption = newValue
defaults.set(newValue.rawValue, forKey: Keys.sortOption)
}
}
/// The current sort direction.
var sortDirection: SortDirection {
get {
if let cached = _sortDirection { return cached }
guard let rawValue = defaults.string(forKey: Keys.sortDirection),
let direction = SortDirection(rawValue: rawValue) else {
return .descending
}
return direction
}
set {
_sortDirection = newValue
defaults.set(newValue.rawValue, forKey: Keys.sortDirection)
}
}
/// Whether to group downloads by channel.
var groupByChannel: Bool {
get {
if let cached = _groupByChannel { return cached }
return defaults.bool(forKey: Keys.groupByChannel)
}
set {
_groupByChannel = newValue
defaults.set(newValue, forKey: Keys.groupByChannel)
}
}
#if os(iOS)
/// Whether to allow downloads on cellular network. Default is false (WiFi only).
var allowCellularDownloads: Bool {
get {
if let cached = _allowCellularDownloads { return cached }
return defaults.bool(forKey: Keys.allowCellularDownloads)
}
set {
_allowCellularDownloads = newValue
defaults.set(newValue, forKey: Keys.allowCellularDownloads)
}
}
#endif
/// Preferred download quality. When set to anything other than .ask,
/// downloads will start automatically without showing the stream selection sheet.
var preferredDownloadQuality: DownloadQuality {
get {
if let cached = _preferredDownloadQuality { return cached }
guard let rawValue = defaults.string(forKey: Keys.preferredQuality),
let quality = DownloadQuality(rawValue: rawValue) else {
return .hd1080p
}
return quality
}
set {
_preferredDownloadQuality = newValue
defaults.set(newValue.rawValue, forKey: Keys.preferredQuality)
}
}
/// Whether to include subtitles when auto-downloading (non-Ask mode).
/// Uses the preferred subtitle language from playback settings.
var includeSubtitlesInAutoDownload: Bool {
get {
if let cached = _includeSubtitlesInAutoDownload { return cached }
return defaults.bool(forKey: Keys.includeSubtitlesInAutoDownload)
}
set {
_includeSubtitlesInAutoDownload = newValue
defaults.set(newValue, forKey: Keys.includeSubtitlesInAutoDownload)
}
}
/// Maximum number of concurrent downloads. Default is 2.
var maxConcurrentDownloads: Int {
get {
if let cached = _maxConcurrentDownloads { return cached }
let value = defaults.integer(forKey: Keys.maxConcurrentDownloads)
return value > 0 ? value : 2
}
set {
_maxConcurrentDownloads = newValue
defaults.set(newValue, forKey: Keys.maxConcurrentDownloads)
}
}
// MARK: - Sorting
/// Sorts an array of downloads based on current settings.
func sorted(_ downloads: [Download]) -> [Download] {
let sorted = downloads.sorted { first, second in
let comparison: Bool
switch sortOption {
case .name:
comparison = first.title.localizedCaseInsensitiveCompare(second.title) == .orderedAscending
case .downloadDate:
let firstDate = first.completedAt ?? first.startedAt ?? Date.distantPast
let secondDate = second.completedAt ?? second.startedAt ?? Date.distantPast
comparison = firstDate < secondDate
case .fileSize:
comparison = first.totalBytes < second.totalBytes
}
return sortDirection == .ascending ? comparison : !comparison
}
return sorted
}
/// Groups downloads by channel.
func groupedByChannel(_ downloads: [Download]) -> [(channel: String, channelID: String, downloads: [Download])] {
let grouped = Dictionary(grouping: downloads) { $0.channelID }
return grouped.map { (channelID, channelDownloads) in
let channelName = channelDownloads.first?.channelName ?? channelID
return (channel: channelName, channelID: channelID, downloads: sorted(channelDownloads))
}
.sorted { $0.channel.localizedCaseInsensitiveCompare($1.channel) == .orderedAscending }
}
}