From d7f41793056ba2af88d1150910765ddaa3325e05 Mon Sep 17 00:00:00 2001 From: Mathijs Bernson Date: Mon, 2 Mar 2026 18:19:00 +0100 Subject: [PATCH] Add app settings tab --- HackerTube/ContentView.swift | 4 ++ HackerTube/Domain/UserDefaultsKey.swift | 15 +++++- .../Features/Settings/SettingsView.swift | 49 +++++++++++++++++ .../VideoPlayer/VideoPlayerView.swift | 15 +++++- HackerTube/HackerTubeAppDelegate.swift | 1 + HackerTube/Localizable.xcstrings | 54 ++++++++++++++++++- 6 files changed, 135 insertions(+), 3 deletions(-) create mode 100644 HackerTube/Features/Settings/SettingsView.swift diff --git a/HackerTube/ContentView.swift b/HackerTube/ContentView.swift index d15d9fa..bfa7f24 100644 --- a/HackerTube/ContentView.swift +++ b/HackerTube/ContentView.swift @@ -27,6 +27,10 @@ struct ContentView: View { ConferencesView() } + Tab("Settings", systemImage: "gear") { + SettingsView() + } + Tab("Search", systemImage: "magnifyingglass", role: .search) { SearchView() } diff --git a/HackerTube/Domain/UserDefaultsKey.swift b/HackerTube/Domain/UserDefaultsKey.swift index 9429a3f..c8066c3 100644 --- a/HackerTube/Domain/UserDefaultsKey.swift +++ b/HackerTube/Domain/UserDefaultsKey.swift @@ -8,5 +8,18 @@ import Foundation enum UserDefaultsKey: String { - case playbackRate = "playbackRate" + case playbackRate + case lowDataModeEnabled +} + +extension UserDefaults { + @objc dynamic var playbackRate: Float { + get { float(forKey: UserDefaultsKey.playbackRate.rawValue) } + set { setValue(newValue, forKey: UserDefaultsKey.playbackRate.rawValue) } + } + + @objc dynamic var lowDataModeEnabled: Bool { + get { bool(forKey: UserDefaultsKey.lowDataModeEnabled.rawValue) } + set { setValue(newValue, forKey: UserDefaultsKey.lowDataModeEnabled.rawValue) } + } } diff --git a/HackerTube/Features/Settings/SettingsView.swift b/HackerTube/Features/Settings/SettingsView.swift new file mode 100644 index 0000000..70bf7af --- /dev/null +++ b/HackerTube/Features/Settings/SettingsView.swift @@ -0,0 +1,49 @@ +// +// SettingsView.swift +// HackerTube +// +// Created by Mathijs Bernson on 02/03/2026. +// + +import SwiftUI +import AVKit + +enum RecordingQuality: String { + case hd, sd +} + +struct SettingsView: View { + @AppStorage(UserDefaultsKey.lowDataModeEnabled.rawValue) + var lowDataModeEnabled = false + + @AppStorage(UserDefaultsKey.playbackRate.rawValue) + var playbackRate: Double = 1.0 + let playbackRates: [Double] = AVPlaybackSpeed.systemDefaultSpeeds.map(\.rate).map(Double.init) + + var body: some View { + NavigationStack { + List { + Section("Playback") { + Picker("Playback speed", selection: $playbackRate) { + ForEach(playbackRates, id: \.self) { playbackRate in + Text("\(playbackRate, format: .number) ×", comment: "Playback rate").tag(playbackRate) + } + } + } + + Section { + Toggle("Low data mode", isOn: $lowDataModeEnabled) + } header: { + Text("Streaming") + } footer: { + Text("When low data mode is on, HackerTube will play standard definition (SD) video instead of HD when possible.") + } + } + .navigationTitle("Settings") + } + } +} + +#Preview { + SettingsView() +} diff --git a/HackerTube/Features/VideoPlayer/VideoPlayerView.swift b/HackerTube/Features/VideoPlayer/VideoPlayerView.swift index 99607c3..d9f6530 100644 --- a/HackerTube/Features/VideoPlayer/VideoPlayerView.swift +++ b/HackerTube/Features/VideoPlayer/VideoPlayerView.swift @@ -6,6 +6,7 @@ // import AVKit +import Combine import SwiftUI import os.log @@ -59,15 +60,27 @@ class VideoPlayerCoordinator: NSObject, AVPlayerViewControllerDelegate { class VideoPlayerViewController: AVPlayerViewController { private var selectedSpeedObserver: NSKeyValueObservation? + private var cancelable: AnyCancellable? override func viewDidLoad() { super.viewDidLoad() + let userDefaults = UserDefaults.standard + selectedSpeedObserver = self.observe(\.selectedSpeed, options: [.new]) { controller, change in if let selectedSpeed = change.newValue.flatMap({ $0 }) { - UserDefaults.standard.set(selectedSpeed.rate, forKey: UserDefaultsKey.playbackRate.rawValue) + userDefaults.set(selectedSpeed.rate, forKey: UserDefaultsKey.playbackRate.rawValue) } } + + cancelable = userDefaults.publisher(for: \.playbackRate) + .sink(receiveValue: { [weak self] newValue in + guard let player = self?.player else { return } + + if newValue != player.rate { + player.rate = newValue + } + }) } override func viewDidAppear(_ animated: Bool) { diff --git a/HackerTube/HackerTubeAppDelegate.swift b/HackerTube/HackerTubeAppDelegate.swift index 31ee762..342c0a8 100644 --- a/HackerTube/HackerTubeAppDelegate.swift +++ b/HackerTube/HackerTubeAppDelegate.swift @@ -16,6 +16,7 @@ class HackerTubeAppDelegate: NSObject, UIApplicationDelegate { private func registerDefaultSettings() { UserDefaults.standard.register(defaults: [ UserDefaultsKey.playbackRate.rawValue : 1.0 as Float, + UserDefaultsKey.lowDataModeEnabled.rawValue : false, ]) } } diff --git a/HackerTube/Localizable.xcstrings b/HackerTube/Localizable.xcstrings index 15922d1..07d4f15 100644 --- a/HackerTube/Localizable.xcstrings +++ b/HackerTube/Localizable.xcstrings @@ -1,6 +1,10 @@ { "sourceLanguage" : "en", "strings" : { + "%@ ×" : { + "comment" : "Playback rate", + "shouldTranslate" : false + }, "%lld views" : { "comment" : "A label displaying the number of views for a talk.", "localizations" : { @@ -147,6 +151,10 @@ } } }, + "Low data mode" : { + "comment" : "A toggle switch that allows the user to enable or disable low data mode.", + "isCommentAutoGenerated" : true + }, "No copyright information encoded in video. Please refer to the website of the organizer of %@ at: %@" : { "comment" : "A label that is shown when no copyright information is available for a talk.", "localizations" : { @@ -199,6 +207,30 @@ } } }, + "Playback" : { + "comment" : "A section header in the settings view related to playback preferences.", + "isCommentAutoGenerated" : true, + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afspelen" + } + } + } + }, + "Playback speed" : { + "comment" : "A picker for selecting the playback speed of video in the settings view.", + "isCommentAutoGenerated" : true, + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Afspeelsnelheid" + } + } + } + }, "Popular" : { "comment" : "Heading for the popular talks view and tab.", "localizations" : { @@ -285,6 +317,26 @@ } } }, + "Settings" : { + "comment" : "The title of the settings tab in the main view.", + "isCommentAutoGenerated" : true, + "localizations" : { + "nl" : { + "stringUnit" : { + "state" : "translated", + "value" : "Instellingen" + } + } + } + }, + "Streaming" : { + "comment" : "A heading for the streaming settings.", + "isCommentAutoGenerated" : true + }, + "When low data mode is on, HackerTube will play standard definition (SD) video instead of HD when possible." : { + "comment" : "A footer text in the settings view that explains the functionality of low data mode.", + "isCommentAutoGenerated" : true + }, "Year" : { "comment" : "A label that indicates the year to filter talks by.", "isCommentAutoGenerated" : true, @@ -304,5 +356,5 @@ } } }, - "version" : "1.0" + "version" : "1.1" } \ No newline at end of file