From 5960f2e3dc61265cd0bd8b46a74cc7a7d1d7665e Mon Sep 17 00:00:00 2001
From: Craig Reyenga <craig.reyenga@gmail.com>
Date: Tue, 11 Mar 2025 14:34:09 -0400
Subject: [PATCH 1/5] Move black theme option to settings screen.

---
 Resources/en.lproj/Localizable.strings        |  2 --
 .../iOS/Settings.bundle/Root.inApp.plist      |  2 --
 .../MediaMoreOptionsActionSheet.swift         |  3 +-
 .../Subviews/MediaPlayerActionSheet.swift     |  5 ---
 .../Controller/SettingsController.swift       |  3 --
 .../Settings/Model/ActionSheetSpecifier.swift | 30 +---------------
 Sources/Settings/Model/SettingsSection.swift  | 34 +++++--------------
 7 files changed, 10 insertions(+), 69 deletions(-)

diff --git a/Resources/en.lproj/Localizable.strings b/Resources/en.lproj/Localizable.strings
index 05401c5a2..dababc2b0 100644
--- a/Resources/en.lproj/Localizable.strings
+++ b/Resources/en.lproj/Localizable.strings
@@ -549,8 +549,6 @@
 "FEEDBACK_EMAIL_NOT_POSSIBLE_TITLE" = "Mail account not configured";
 "FEEDBACK_EMAIL_NOT_POSSIBLE_LONG" = "Contact us through %@ from another device.";
 
-"SETTINGS_THEME_BLACK" = "Use black background on dark mode";
-"SETTINGS_THEME_BLACK_SUBTITLE" = "Improves battery life on devices with an OLED screen";
 
 "SETTINGS_RESET_TITLE" = "Reset the settings";
 "SETTINGS_RESET_MESSAGE" = "Do you want to reset all the settings to their default values?";
diff --git a/Resources/iOS/Settings.bundle/Root.inApp.plist b/Resources/iOS/Settings.bundle/Root.inApp.plist
index 2172d720b..8ba36216e 100644
--- a/Resources/iOS/Settings.bundle/Root.inApp.plist
+++ b/Resources/iOS/Settings.bundle/Root.inApp.plist
@@ -42,14 +42,12 @@
 				<string>SETTINGS_THEME_BRIGHT</string>
 				<string>SETTINGS_THEME_DARK</string>
 				<string>SETTINGS_THEME_SYSTEM</string>
-				<string>SETTINGS_THEME_BLACK</string>
 			</array>
 			<key>Values</key>
 			<array>
 				<integer>0</integer>
 				<integer>1</integer>
 				<integer>2</integer>
-				<string>3</string>
 			</array>
 		</dict>
 		<dict>
diff --git a/Sources/Playback/Player/VideoPlayer-iOS/MediaMoreOptionsActionSheet.swift b/Sources/Playback/Player/VideoPlayer-iOS/MediaMoreOptionsActionSheet.swift
index 0e587c057..9ab18d603 100644
--- a/Sources/Playback/Player/VideoPlayer-iOS/MediaMoreOptionsActionSheet.swift
+++ b/Sources/Playback/Player/VideoPlayer-iOS/MediaMoreOptionsActionSheet.swift
@@ -486,8 +486,7 @@ extension MediaMoreOptionsActionSheet: MediaPlayerActionSheetDataSource {
             }
 
             // Do not display these options in the action sheet.
-            if $0 == .addBookmarks || $0 == .blackBackground ||
-                $0 == .playNextItem || $0 == .playlistPlayNextItem {
+            if [ .addBookmarks, .playNextItem, .playlistPlayNextItem ].contains($0) {
                 return
             }
 
diff --git a/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaPlayerActionSheet.swift b/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaPlayerActionSheet.swift
index 9783bb165..21d043f20 100644
--- a/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaPlayerActionSheet.swift
+++ b/Sources/Playback/Player/VideoPlayer-iOS/Subviews/MediaPlayerActionSheet.swift
@@ -22,7 +22,6 @@ enum ActionSheetCellIdentifier: String, CustomStringConvertible, CaseIterable {
     case addBookmarks
     case abRepeat
     case interfaceLock
-    case blackBackground
     case playNextItem
     case playlistPlayNextItem
 
@@ -48,8 +47,6 @@ enum ActionSheetCellIdentifier: String, CustomStringConvertible, CaseIterable {
             return NSLocalizedString("REPEAT_MODE", comment: "")
         case .abRepeat:
             return NSLocalizedString("AB_LOOP", comment: "")
-        case .blackBackground:
-            return NSLocalizedString("SETTINGS_THEME_BLACK", comment: "")
         case .playNextItem:
             return NSLocalizedString("SETTINGS_PLAY_ALL", comment: "")
         case .playlistPlayNextItem:
@@ -73,8 +70,6 @@ enum ActionSheetCellIdentifier: String, CustomStringConvertible, CaseIterable {
             return NSLocalizedString("BOOKMARKS_HINT", comment: "")
         case .interfaceLock:
             return NSLocalizedString("INTERFACE_LOCK_HINT", comment: "")
-        case .blackBackground:
-            return NSLocalizedString("SETTINGS_THEME_BLACK_SUBTITLE", comment: "")
         case .playNextItem:
             return NSLocalizedString("SETTINGS_PLAY_ALL_HINT", comment: "")
         case .playlistPlayNextItem:
diff --git a/Sources/Settings/Controller/SettingsController.swift b/Sources/Settings/Controller/SettingsController.swift
index bd9aa32be..ae55ad1c3 100644
--- a/Sources/Settings/Controller/SettingsController.swift
+++ b/Sources/Settings/Controller/SettingsController.swift
@@ -509,9 +509,6 @@ extension SettingsController {
 extension SettingsController: ActionSheetSpecifierDelegate {
     func actionSheetSpecifierHandleToggleSwitch(for cell: ActionSheetCell, state: Bool) {
         switch cell.identifier {
-        case .blackBackground:
-            userDefaults.setValue(state, forKey: kVLCSettingAppThemeBlack)
-            PresentationTheme.themeDidUpdate()
         case .playNextItem:
             userDefaults.setValue(state, forKey: kVLCAutomaticallyPlayNextItem)
         case .playlistPlayNextItem:
diff --git a/Sources/Settings/Model/ActionSheetSpecifier.swift b/Sources/Settings/Model/ActionSheetSpecifier.swift
index ed4e4db9f..e2fd01020 100644
--- a/Sources/Settings/Model/ActionSheetSpecifier.swift
+++ b/Sources/Settings/Model/ActionSheetSpecifier.swift
@@ -60,12 +60,6 @@ extension ActionSheetSpecifier: ActionSheetDelegate {
             return
         }
 
-        guard preferenceKey != kVLCSettingAppTheme ||
-                (!PresentationTheme.current.isDark || indexPath.row != numberOfRows() - 1) else {
-            // Disable the selection for the black background option cell in the appearance action sheet
-            return
-        }
-
         guard preferenceKey != kVLCAutomaticallyPlayNextItem else {
             // Disable the selection for the automatically play next item options
             return
@@ -106,15 +100,6 @@ extension ActionSheetSpecifier: ActionSheetDataSource {
             return 0
         }
 
-        if preferenceKey == kVLCSettingAppTheme {
-            let isThemeDark: Bool = PresentationTheme.current.isDark
-            if #available(iOS 13, *) {
-                return isThemeDark ? rowCount : rowCount - 1
-            } else {
-                return isThemeDark ? rowCount - 1 : rowCount - 2
-            }
-        }
-
         return rowCount
     }
 
@@ -128,20 +113,7 @@ extension ActionSheetSpecifier: ActionSheetDataSource {
             return UICollectionViewCell()
         }
 
-        if preferenceKey == kVLCSettingAppTheme &&
-            PresentationTheme.current.isDark && indexPath.row == numberOfRows() - 1 {
-            // Update the black background option cell
-            cell.setAccessoryType(to: .toggleSwitch)
-            cell.setToggleSwitch(state: UserDefaults.standard.bool(forKey: kVLCSettingAppThemeBlack))
-            cell.name.text = settingsBundle.localizedString(forKey: "SETTINGS_THEME_BLACK", value: "", table: "Root")
-            let cellIdentifier = ActionSheetCellIdentifier.blackBackground
-            cell.identifier = cellIdentifier
-            cell.name.accessibilityLabel = cellIdentifier.description
-            cell.name.accessibilityHint = cellIdentifier.accessibilityHint
-            cell.delegate = self
-
-            return cell
-        } else if preferenceKey == kVLCAutomaticallyPlayNextItem {
+        if preferenceKey == kVLCAutomaticallyPlayNextItem {
             cell.setAccessoryType(to: .toggleSwitch)
             let isFirstRow: Bool = indexPath.row == 0
 
diff --git a/Sources/Settings/Model/SettingsSection.swift b/Sources/Settings/Model/SettingsSection.swift
index a7b6ffbe5..5b40cb6f4 100644
--- a/Sources/Settings/Model/SettingsSection.swift
+++ b/Sources/Settings/Model/SettingsSection.swift
@@ -44,7 +44,7 @@ struct SettingsItem: Equatable {
         self.isTitleEmphasized = isTitleEmphasized
     }
 
-    static func toggle(title: String, subtitle: String?, preferenceKey: String) -> Self {
+    static func toggle(title: String, subtitle: String? = nil, preferenceKey: String) -> Self {
         return Self(title: title, subtitle: subtitle, action: .toggle(Toggle(preferenceKey: preferenceKey)))
     }
 
@@ -176,10 +176,17 @@ enum MainOptions {
                      action: .showActionSheet(title: "SETTINGS_DARKTHEME", preferenceKey: k, hasInfo: false))
     }
 
+    static var blackTheme: SettingsItem {
+        .toggle(title: "SETTINGS_THEME_BLACK",
+                subtitle: "SETTINGS_THEME_BLACK_SUBTITLE",
+                preferenceKey: kVLCSettingAppThemeBlack)
+    }
+
     static func section() -> SettingsSection? {
         .init(title: nil, items: [
             privacy,
             appearance,
+            blackTheme
         ])
     }
 }
@@ -217,7 +224,6 @@ enum GenericOptions {
 
     static var playVideoInFullScreen: SettingsItem {
         .toggle(title: "SETTINGS_VIDEO_FULLSCREEN",
-                subtitle: nil,
                 preferenceKey: kVLCSettingVideoFullscreenPlayback)
     }
 
@@ -237,19 +243,16 @@ enum GenericOptions {
 
     static var enableTextScrollingInMediaList: SettingsItem {
         .toggle(title: "SETTINGS_ENABLE_MEDIA_CELL_TEXT_SCROLLING",
-                subtitle: nil,
                 preferenceKey: kVLCSettingEnableMediaCellTextScrolling)
     }
 
     static var rememberPlayerState: SettingsItem {
         .toggle(title: "SETTINGS_REMEMBER_PLAYER_STATE",
-                subtitle: nil,
                 preferenceKey: kVLCPlayerShouldRememberState)
     }
 
     static var restoreLastPlayedMedia: SettingsItem {
         .toggle(title: "SETTINGS_RESTORE_LAST_PLAYED_MEDIA",
-                subtitle: nil,
                 preferenceKey: kVLCRestoreLastPlayedMedia)
     }
 
@@ -323,43 +326,36 @@ enum PrivacyOptions {
 enum GestureControlOptions {
     static var swipeUpDownForVolume: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_VOLUME",
-                subtitle: nil,
                 preferenceKey: kVLCSettingVolumeGesture)
     }
 
     static var twoFingerTap: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_PLAYPAUSE",
-                subtitle: nil,
                 preferenceKey: kVLCSettingPlayPauseGesture)
     }
 
     static var swipeUpDownForBrightness: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_BRIGHTNESS",
-                subtitle: nil,
                 preferenceKey: kVLCSettingBrightnessGesture)
     }
 
     static var swipeRightLeftToSeek: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_SEEK",
-                subtitle: nil,
                 preferenceKey: kVLCSettingSeekGesture)
     }
 
     static var pinchToClose: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_CLOSE",
-                subtitle: nil,
                 preferenceKey: kVLCSettingCloseGesture)
     }
 
     static var forwardBackwardEqual: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_FORWARD_BACKWARD_EQUAL",
-                subtitle: nil,
                 preferenceKey: kVLCSettingPlaybackForwardBackwardEqual)
     }
 
     static var tapSwipeEqual: SettingsItem {
         .toggle(title: "SETTINGS_GESTURES_TAP_SWIPE_EQUAL",
-                subtitle: nil,
                 preferenceKey: kVLCSettingPlaybackTapSwipeEqual)
     }
 
@@ -393,7 +389,6 @@ enum GestureControlOptions {
 
     static var longTouchToSpeedUp: SettingsItem {
         .toggle(title: "SETINGS_LONG_TOUCH_SPEED_UP",
-                subtitle: nil,
                 preferenceKey: kVLCSettingPlaybackLongTouchSpeedUp)
     }
 
@@ -492,13 +487,11 @@ enum VideoOptions {
 
     static var rememberPlayerBrightness: SettingsItem {
         .toggle(title: "SETTINGS_REMEMBER_PLAYER_BRIGHTNESS",
-                subtitle: nil,
                 preferenceKey: kVLCPlayerShouldRememberBrightness)
     }
 
     static var lockRotation: SettingsItem {
         .toggle(title: "SETTINGS_LOCK_ROTATION",
-                subtitle: nil,
                 preferenceKey: kVLCSettingRotationLock)
     }
 
@@ -538,7 +531,6 @@ enum SubtitlesOptions {
 
     static var useBoldFont: SettingsItem {
         .toggle(title: "SETTINGS_SUBTITLES_BOLDFONT",
-                subtitle: nil,
                 preferenceKey: kVLCSettingSubtitlesBoldFont)
     }
 
@@ -610,7 +602,6 @@ enum AudioOptions {
 
     static var audioPlaybackInBackground: SettingsItem {
         .toggle(title: "SETTINGS_BACKGROUND_AUDIO",
-                subtitle: nil,
                 preferenceKey: kVLCSettingContinueAudioInBackgroundKey)
     }
 
@@ -635,31 +626,26 @@ enum MediaLibraryOptions {
 
     static var optimiseItemNamesForDisplay: SettingsItem {
         .toggle(title: "SETTINGS_DECRAPIFY",
-                subtitle: nil,
                 preferenceKey: kVLCSettingsDecrapifyTitles)
     }
 
     static var disableGrouping: SettingsItem {
         .toggle(title: "SETTINGS_DISABLE_GROUPING",
-                subtitle: nil,
                 preferenceKey: kVLCSettingsDisableGrouping)
     }
 
     static var showVideoThumbnails: SettingsItem {
         .toggle(title: "SETTINGS_SHOW_THUMBNAILS",
-                subtitle: nil,
                 preferenceKey: kVLCSettingShowThumbnails)
     }
 
     static var showAudioArtworks: SettingsItem {
         .toggle(title: "SETTINGS_SHOW_ARTWORKS",
-                subtitle: nil,
                 preferenceKey: kVLCSettingShowArtworks)
     }
 
     static var includeMediaLibInDeviceBackup: SettingsItem {
         .toggle(title: "SETTINGS_BACKUP_MEDIA_LIBRARY",
-                subtitle: nil,
                 preferenceKey: kVLCSettingBackupMediaLibrary)
     }
 
@@ -698,7 +684,6 @@ enum NetworkOptions {
 
     static var ipv6SupportForWiFiSharing: SettingsItem {
         .toggle(title: "SETTINGS_WIFISHARING_IPv6",
-                subtitle: nil,
                 preferenceKey: kVLCSettingWiFiSharingIPv6)
     }
 
@@ -710,7 +695,6 @@ enum NetworkOptions {
 
     static var rtspctp: SettingsItem {
         .toggle(title: "SETTINGS_RTSP_TCP",
-                subtitle: nil,
                 preferenceKey: kVLCSettingNetworkRTSPTCP)
     }
 
@@ -736,7 +720,6 @@ enum Accessibility {
 
     static var pauseWhenShowingControls: SettingsItem {
         .toggle(title: "SETTINGS_PAUSE_WHEN_SHOWING_CONTROLS",
-                subtitle: nil,
                 preferenceKey: kVLCSettingPauseWhenShowingControls)
     }
 
@@ -753,7 +736,6 @@ enum Accessibility {
 enum Lab {
     static var debugLogging: SettingsItem {
         .toggle(title: "SETTINGS_DEBUG_LOG",
-                subtitle: nil,
                 preferenceKey: kVLCSaveDebugLogs)
     }
 
-- 
GitLab


From 448dfaa249c3d881625987b950f5a8f5a0732783 Mon Sep 17 00:00:00 2001
From: Craig Reyenga <craig.reyenga@gmail.com>
Date: Tue, 11 Mar 2025 15:07:54 -0400
Subject: [PATCH 2/5] Introduced concept of enabled or disabled settings items.
 Enforce with black theme option.

---
 Sources/Settings/Model/SettingsSection.swift | 11 +++++++----
 Sources/Settings/View/SettingsCell.swift     | 10 ++++++++++
 2 files changed, 17 insertions(+), 4 deletions(-)

diff --git a/Sources/Settings/Model/SettingsSection.swift b/Sources/Settings/Model/SettingsSection.swift
index 5b40cb6f4..c0c76f41e 100644
--- a/Sources/Settings/Model/SettingsSection.swift
+++ b/Sources/Settings/Model/SettingsSection.swift
@@ -23,6 +23,7 @@ struct SettingsItem: Equatable {
     let title: String
     let subtitle: String?
     let action: Action
+    let isEnabled: Bool
     let isTitleEmphasized: Bool
 
     @available(*, deprecated, message: "access from self.action")
@@ -37,15 +38,16 @@ struct SettingsItem: Equatable {
         }
     }
 
-    init(title: String, subtitle: String?, action: Action, isTitleEmphasized: Bool = false) {
+    init(title: String, subtitle: String?, action: Action, isEnabled: Bool = true, isTitleEmphasized: Bool = false) {
         self.title = Localizer.localizedTitle(key: title)
         self.subtitle = subtitle.flatMap(Localizer.localizedTitle(key:))
         self.action = action
+        self.isEnabled = isEnabled
         self.isTitleEmphasized = isTitleEmphasized
     }
 
-    static func toggle(title: String, subtitle: String? = nil, preferenceKey: String) -> Self {
-        return Self(title: title, subtitle: subtitle, action: .toggle(Toggle(preferenceKey: preferenceKey)))
+    static func toggle(title: String, subtitle: String? = nil, preferenceKey: String, isEnabled: Bool = true) -> Self {
+        return Self(title: title, subtitle: subtitle, action: .toggle(Toggle(preferenceKey: preferenceKey)), isEnabled: isEnabled)
     }
 
     enum Action: Equatable {
@@ -179,7 +181,8 @@ enum MainOptions {
     static var blackTheme: SettingsItem {
         .toggle(title: "SETTINGS_THEME_BLACK",
                 subtitle: "SETTINGS_THEME_BLACK_SUBTITLE",
-                preferenceKey: kVLCSettingAppThemeBlack)
+                preferenceKey: kVLCSettingAppThemeBlack,
+                isEnabled: UserDefaults.standard.integer(forKey: kVLCSettingAppTheme) != kVLCSettingAppThemeBright)
     }
 
     static func section() -> SettingsSection? {
diff --git a/Sources/Settings/View/SettingsCell.swift b/Sources/Settings/View/SettingsCell.swift
index 0f9a69edc..356c0b8c0 100644
--- a/Sources/Settings/View/SettingsCell.swift
+++ b/Sources/Settings/View/SettingsCell.swift
@@ -30,6 +30,7 @@ class SettingsCell: UITableViewCell {
         static let marginBottom: CGFloat = 10
         static let marginLeading: CGFloat = 20
         static let marginTrailing: CGFloat = 70
+        static let disabledAlpha: CGFloat = 0.3
     }
 
     weak var delegate: SettingsCellDelegate?
@@ -208,6 +209,15 @@ class SettingsCell: UITableViewCell {
             } else {
                 activityIndicator.stopAnimating()
             }
+
+            if settingsItem.isEnabled {
+                switchControl.isEnabled = true
+                contentView.alpha = 1
+            } else {
+                switchControl.isEnabled = false
+                contentView.alpha = Constants.disabledAlpha
+            }
+
         }
     }
 
-- 
GitLab


From f5ff51cd73c7f5d3ea49625f0aa05ebe160536ad Mon Sep 17 00:00:00 2001
From: Craig Reyenga <craig.reyenga@gmail.com>
Date: Tue, 11 Mar 2025 16:37:00 -0400
Subject: [PATCH 3/5] Fix issues with theme settings updates, especially in the
 settings screen itself.

---
 Sources/App/iOS/DefaultsChangeListener.swift  | 66 +++++++++++++++++++
 Sources/App/iOS/VLCAppCoordinator.m           |  3 +
 .../Controller/SettingsController.swift       |  9 ++-
 .../Settings/Model/ActionSheetSpecifier.swift |  4 --
 Sources/Settings/View/SettingsCell.swift      |  4 ++
 Sources/UI Elements/PresentationTheme.swift   | 48 ++++++++------
 VLC.xcodeproj/project.pbxproj                 |  6 ++
 7 files changed, 114 insertions(+), 26 deletions(-)
 create mode 100644 Sources/App/iOS/DefaultsChangeListener.swift

diff --git a/Sources/App/iOS/DefaultsChangeListener.swift b/Sources/App/iOS/DefaultsChangeListener.swift
new file mode 100644
index 000000000..1abf1fd7a
--- /dev/null
+++ b/Sources/App/iOS/DefaultsChangeListener.swift
@@ -0,0 +1,66 @@
+/*****************************************************************************
+ * DefaultsChangeListener.swift
+ * VLC for iOS
+ *****************************************************************************
+ * Copyright (c) 2025 VideoLAN. All rights reserved.
+ * $Id$
+ *
+ * Authors: Craig Reyenga <craig.reyenga # gmail.com>
+ *
+ * Refer to the COPYING file of the official project for license.
+ *****************************************************************************/
+
+/// Emits notifications or performs other actions based on updates to user defaults.
+@objc(VLCDefaultsChangeListener)
+final class DefaultsChangeListener: NSObject {
+    private var appTheme: ChangeManager<Int>
+    private var appThemeBlack: ChangeManager<Int>
+
+    override init() {
+        appTheme = ChangeManager(value: Self.readAppTheme(), action: { _ in
+            PresentationTheme.themeDidUpdate()
+        })
+
+        appThemeBlack = ChangeManager(value: Self.readAppThemeBlack(), action: { _ in
+            PresentationTheme.themeDidUpdate()
+        })
+
+        super.init()
+
+        let notificationCenter = NotificationCenter.default
+        notificationCenter.addObserver(self,
+                                       selector: #selector(userDefaultsUpdated),
+                                       name: UserDefaults.didChangeNotification,
+                                       object: nil)
+    }
+
+    @objc private func userDefaultsUpdated() {
+        appTheme.update(Self.readAppTheme())
+        appThemeBlack.update(Self.readAppThemeBlack())
+    }
+
+    static func readAppTheme() -> Int {
+        return UserDefaults.standard.integer(forKey: kVLCSettingAppTheme)
+    }
+
+    static func readAppThemeBlack() -> Int {
+        return UserDefaults.standard.integer(forKey: kVLCSettingAppThemeBlack)
+    }
+}
+
+/// Executes an action when the underlying value has changed.
+fileprivate final class ChangeManager<T: Equatable> {
+    var value: T
+    let action: (T) -> Void
+
+    init(value: T, action: @escaping (T) -> Void) {
+        self.value = value
+        self.action = action
+    }
+
+    func update(_ newValue: T) {
+        guard value != newValue else { return }
+        value = newValue
+        action(newValue)
+    }
+}
diff --git a/Sources/App/iOS/VLCAppCoordinator.m b/Sources/App/iOS/VLCAppCoordinator.m
index 0b0b8c2d8..14c69ed31 100644
--- a/Sources/App/iOS/VLCAppCoordinator.m
+++ b/Sources/App/iOS/VLCAppCoordinator.m
@@ -28,6 +28,7 @@
     VLCRemoteControlService *_remoteControlService;
     UIWindow *_externalWindow;
     VLCStripeController *_stripeController;
+    VLCDefaultsChangeListener *_defaultsChangeListener;
 
 #if TARGET_OS_IOS
     VLCRendererDiscovererManager *_rendererDiscovererManager;
@@ -54,6 +55,8 @@
 {
     self = [super init];
     if (self) {
+        _defaultsChangeListener = [[VLCDefaultsChangeListener alloc] init];
+
         dispatch_async(dispatch_get_main_queue(), ^{
             [VLCLibrary setSharedEventsConfiguration:[VLCEventsLegacyConfiguration new]];
             [self initializeServices];
diff --git a/Sources/Settings/Controller/SettingsController.swift b/Sources/Settings/Controller/SettingsController.swift
index ae55ad1c3..ca7965c63 100644
--- a/Sources/Settings/Controller/SettingsController.swift
+++ b/Sources/Settings/Controller/SettingsController.swift
@@ -169,7 +169,14 @@ class SettingsController: UITableViewController {
 #if os(iOS)
         setNeedsStatusBarAppearanceUpdate()
 #endif
-        reloadSettingsSections() // When theme changes hide the black theme section if needed
+
+        tableView.visibleCells.forEach { cell in
+            guard let cell = cell as? SettingsCell else { return }
+
+            cell.themeChanged()
+        }
+
+        reloadSettingsSections()
     }
 
     @objc private func miniPlayerIsShown() {
diff --git a/Sources/Settings/Model/ActionSheetSpecifier.swift b/Sources/Settings/Model/ActionSheetSpecifier.swift
index e2fd01020..1b6c16b77 100644
--- a/Sources/Settings/Model/ActionSheetSpecifier.swift
+++ b/Sources/Settings/Model/ActionSheetSpecifier.swift
@@ -67,10 +67,6 @@ extension ActionSheetSpecifier: ActionSheetDelegate {
 
         userDefaults.set(settingSpecifier?.specifier[indexPath.row].value, forKey: preferenceKey)
 
-        if preferenceKey == kVLCSettingAppTheme {
-            PresentationTheme.themeDidUpdate()
-        }
-
 #if os(iOS)
         NotificationFeedbackGenerator().success()
 #endif
diff --git a/Sources/Settings/View/SettingsCell.swift b/Sources/Settings/View/SettingsCell.swift
index 356c0b8c0..7d83ea10d 100644
--- a/Sources/Settings/View/SettingsCell.swift
+++ b/Sources/Settings/View/SettingsCell.swift
@@ -231,6 +231,10 @@ class SettingsCell: UITableViewCell {
         fatalError("init(coder:) has not been implemented")
     }
 
+    func themeChanged() {
+        setupTheme()
+    }
+
     override func prepareForReuse() {
         super.prepareForReuse()
         backgroundColor = .clear // Required to prevent theme mismatch during setupTheme
diff --git a/Sources/UI Elements/PresentationTheme.swift b/Sources/UI Elements/PresentationTheme.swift
index 10585824d..7fa2e8c33 100644
--- a/Sources/UI Elements/PresentationTheme.swift	
+++ b/Sources/UI Elements/PresentationTheme.swift	
@@ -43,27 +43,27 @@ extension Notification.Name {
     let thumbnailBackgroundColor: UIColor
 
     init(isDark: Bool,
-                name: String,
-                statusBarStyle: UIStatusBarStyle,
-                navigationbarColor: UIColor,
-                navigationbarTextColor: UIColor,
-                background: UIColor,
-                cellBackgroundA: UIColor,
-                cellBackgroundB: UIColor,
-                cellDetailTextColor: UIColor,
-                cellTextColor: UIColor,
-                lightTextColor: UIColor,
-                sectionHeaderTextColor: UIColor,
-                separatorColor: UIColor,
-                mediaCategorySeparatorColor: UIColor,
-                tabBarColor: UIColor,
-                orangeUI: UIColor,
-                orangeDarkAccent: UIColor,
-                toolBarStyle: UIBarStyle,
-                blurStyle: UIBlurEffect.Style,
-                textfieldBorderColor: UIColor,
-                textfieldPlaceholderColor: UIColor,
-                thumbnailBackgroundColor: UIColor) {
+         name: String,
+         statusBarStyle: UIStatusBarStyle,
+         navigationbarColor: UIColor,
+         navigationbarTextColor: UIColor,
+         background: UIColor,
+         cellBackgroundA: UIColor,
+         cellBackgroundB: UIColor,
+         cellDetailTextColor: UIColor,
+         cellTextColor: UIColor,
+         lightTextColor: UIColor,
+         sectionHeaderTextColor: UIColor,
+         separatorColor: UIColor,
+         mediaCategorySeparatorColor: UIColor,
+         tabBarColor: UIColor,
+         orangeUI: UIColor,
+         orangeDarkAccent: UIColor,
+         toolBarStyle: UIBarStyle,
+         blurStyle: UIBlurEffect.Style,
+         textfieldBorderColor: UIColor,
+         textfieldPlaceholderColor: UIColor,
+         thumbnailBackgroundColor: UIColor) {
         self.isDark = isDark
         self.name = name
         self.statusBarStyle = statusBarStyle
@@ -89,6 +89,8 @@ extension Notification.Name {
     }
 }
 
+// MARK: - Typography
+
 @objcMembers class Typography: NSObject {
     
     let tableHeaderFont: UIFont
@@ -106,6 +108,8 @@ enum PresentationThemeType: Int {
     case auto
 }
 
+// MARK: - PresentationTheme
+
 @objcMembers class PresentationTheme: NSObject {
 
     static let brightTheme = PresentationTheme(colors: brightPalette)
@@ -182,6 +186,8 @@ enum PresentationThemeType: Int {
     let font = defaultFont
 }
 
+// MARK: - UIColor
+
 @objc extension UIColor {
 
     convenience init(_ rgbValue: UInt32, _ alpha: CGFloat = 1.0) {
diff --git a/VLC.xcodeproj/project.pbxproj b/VLC.xcodeproj/project.pbxproj
index 4f743280f..ee52f64ec 100644
--- a/VLC.xcodeproj/project.pbxproj
+++ b/VLC.xcodeproj/project.pbxproj
@@ -96,6 +96,8 @@
 		444E5BFA24C6081B0003B69C /* PasscodeLockController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444E5BF924C6081A0003B69C /* PasscodeLockController.swift */; };
 		444E5C0024C719480003B69C /* AboutController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 444E5BFF24C719480003B69C /* AboutController.swift */; };
 		44B5822024E434FD001A2583 /* MediaGridCollectionCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44B5821F24E434FD001A2583 /* MediaGridCollectionCell.swift */; };
+		44BA0BD62D80CF4D004CF52E /* DefaultsChangeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BA0BD52D80CF20004CF52E /* DefaultsChangeListener.swift */; };
+		44BA0BD72D80CF4D004CF52E /* DefaultsChangeListener.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44BA0BD52D80CF20004CF52E /* DefaultsChangeListener.swift */; };
 		44C8BBA324AF2B5C003F8940 /* FeedbackGenerators.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8BBA224AF2B5C003F8940 /* FeedbackGenerators.swift */; };
 		44C8BBAE24AF36F4003F8940 /* SettingsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8BBA624AF36F4003F8940 /* SettingsController.swift */; };
 		44C8BBAF24AF36F4003F8940 /* SettingsSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 44C8BBA824AF36F4003F8940 /* SettingsSection.swift */; };
@@ -1013,6 +1015,7 @@
 		444E5BF924C6081A0003B69C /* PasscodeLockController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PasscodeLockController.swift; sourceTree = "<group>"; };
 		444E5BFF24C719480003B69C /* AboutController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AboutController.swift; sourceTree = "<group>"; };
 		44B5821F24E434FD001A2583 /* MediaGridCollectionCell.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = MediaGridCollectionCell.swift; sourceTree = "<group>"; };
+		44BA0BD52D80CF20004CF52E /* DefaultsChangeListener.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DefaultsChangeListener.swift; sourceTree = "<group>"; };
 		44C8BBA224AF2B5C003F8940 /* FeedbackGenerators.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FeedbackGenerators.swift; sourceTree = "<group>"; };
 		44C8BBA624AF36F4003F8940 /* SettingsController.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsController.swift; sourceTree = "<group>"; };
 		44C8BBA824AF36F4003F8940 /* SettingsSection.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = SettingsSection.swift; sourceTree = "<group>"; };
@@ -3100,6 +3103,7 @@
 				418B144C20179C74000447AA /* TabBarCoordinator.swift */,
 				8F9108342A67AC76007EB0D5 /* SirikitIntentCoordinator.swift */,
 				91562F582CAEBC1500D42986 /* PlayMediaIntent.swift */,
+				44BA0BD52D80CF20004CF52E /* DefaultsChangeListener.swift */,
 			);
 			path = iOS;
 			sourceTree = "<group>";
@@ -4444,6 +4448,7 @@
 				7D50C6902BBD20DF00B9F1A0 /* VLCLocalNetworkServiceNetService.m in Sources */,
 				7D274D582D2AE81C00ADDC41 /* LongPressPlaybackSpeedView.swift in Sources */,
 				7D50C6912BBD20DF00B9F1A0 /* VLCPlexWebAPI.m in Sources */,
+				44BA0BD72D80CF4D004CF52E /* DefaultsChangeListener.swift in Sources */,
 				7D50C6922BBD20DF00B9F1A0 /* VLCDonationNagScreenViewController.m in Sources */,
 				7D50C6932BBD20DF00B9F1A0 /* ButtonBarView.swift in Sources */,
 				7D50C6942BBD20DF00B9F1A0 /* MediaPlayerActionSheet.swift in Sources */,
@@ -4641,6 +4646,7 @@
 				DD3EFF411BDEBCE500B68579 /* VLCNetworkServerBrowserSharedLibrary.m in Sources */,
 				7DC1865A2A0BB0C3009E84E1 /* UIImage+Gradient.swift in Sources */,
 				8D15A1F022B0FB9300CFA758 /* QueueViewController.swift in Sources */,
+				44BA0BD62D80CF4D004CF52E /* DefaultsChangeListener.swift in Sources */,
 				D96C9EBC28105F5B005F13BB /* AlbumHeader.swift in Sources */,
 				418E88412110E51B00DDA6A7 /* URLHandler.swift in Sources */,
 				8DE187812105DAB100A091D2 /* VideoViewController.swift in Sources */,
-- 
GitLab


From 40a2608bf00e702c786b410081f94b8a3bc04e06 Mon Sep 17 00:00:00 2001
From: Craig Reyenga <craig.reyenga@gmail.com>
Date: Wed, 12 Mar 2025 09:45:55 -0400
Subject: [PATCH 4/5] Move disableGrouping into DefaultsChangeListener.

---
 Sources/App/iOS/DefaultsChangeListener.swift    | 17 ++++++++++++++---
 .../Controller/SettingsController.swift         |  8 --------
 2 files changed, 14 insertions(+), 11 deletions(-)

diff --git a/Sources/App/iOS/DefaultsChangeListener.swift b/Sources/App/iOS/DefaultsChangeListener.swift
index 1abf1fd7a..cc0260f40 100644
--- a/Sources/App/iOS/DefaultsChangeListener.swift
+++ b/Sources/App/iOS/DefaultsChangeListener.swift
@@ -13,10 +13,13 @@
 /// Emits notifications or performs other actions based on updates to user defaults.
 @objc(VLCDefaultsChangeListener)
 final class DefaultsChangeListener: NSObject {
-    private var appTheme: ChangeManager<Int>
-    private var appThemeBlack: ChangeManager<Int>
+    private let appTheme: ChangeManager<Int>
+    private let appThemeBlack: ChangeManager<Int>
+    private let disableGrouping: ChangeManager<Bool>
 
     override init() {
+        let notificationCenter = NotificationCenter.default
+
         appTheme = ChangeManager(value: Self.readAppTheme(), action: { _ in
             PresentationTheme.themeDidUpdate()
         })
@@ -25,9 +28,12 @@ final class DefaultsChangeListener: NSObject {
             PresentationTheme.themeDidUpdate()
         })
 
+        disableGrouping = ChangeManager(value: Self.readDisableGrouping(), action: { _ in
+            notificationCenter.post(name: .VLCDisableGroupingDidChangeNotification, object: nil)
+        })
+
         super.init()
 
-        let notificationCenter = NotificationCenter.default
         notificationCenter.addObserver(self,
                                        selector: #selector(userDefaultsUpdated),
                                        name: UserDefaults.didChangeNotification,
@@ -37,6 +43,7 @@ final class DefaultsChangeListener: NSObject {
     @objc private func userDefaultsUpdated() {
         appTheme.update(Self.readAppTheme())
         appThemeBlack.update(Self.readAppThemeBlack())
+        disableGrouping.update(Self.readDisableGrouping())
     }
 
     static func readAppTheme() -> Int {
@@ -46,6 +53,10 @@ final class DefaultsChangeListener: NSObject {
     static func readAppThemeBlack() -> Int {
         return UserDefaults.standard.integer(forKey: kVLCSettingAppThemeBlack)
     }
+
+    static func readDisableGrouping() -> Bool {
+        return UserDefaults.standard.bool(forKey: kVLCSettingsDisableGrouping)
+    }
 }
 
 /// Executes an action when the underlying value has changed.
diff --git a/Sources/Settings/Controller/SettingsController.swift b/Sources/Settings/Controller/SettingsController.swift
index ca7965c63..4435b1f2a 100644
--- a/Sources/Settings/Controller/SettingsController.swift
+++ b/Sources/Settings/Controller/SettingsController.swift
@@ -443,8 +443,6 @@ extension SettingsController: SettingsCellDelegate {
             medialibraryHidingLockSwitchOn(state: isOn)
         case kVLCSettingBackupMediaLibrary:
             mediaLibraryBackupActivateSwitchOn(state: isOn)
-        case kVLCSettingsDisableGrouping:
-            medialibraryDisableGroupingSwitchOn(state: isOn)
         default:
             break
         }
@@ -507,12 +505,6 @@ extension SettingsController {
     }
 }
 
-extension SettingsController {
-    func medialibraryDisableGroupingSwitchOn(state _: Bool) {
-        notificationCenter.post(name: .VLCDisableGroupingDidChangeNotification, object: self)
-    }
-}
-
 extension SettingsController: ActionSheetSpecifierDelegate {
     func actionSheetSpecifierHandleToggleSwitch(for cell: ActionSheetCell, state: Bool) {
         switch cell.identifier {
-- 
GitLab


From 8d47fde88baecf23d3165257cd11a279342b17c1 Mon Sep 17 00:00:00 2001
From: Craig Reyenga <craig.reyenga@gmail.com>
Date: Wed, 12 Mar 2025 10:24:17 -0400
Subject: [PATCH 5/5] Move hideMediaLibrary and excludeFromDeviceBackup into
 DefaultsChangeListener.

---
 Sources/App/iOS/DefaultsChangeListener.swift  | 35 +++++++++++++++++--
 Sources/App/iOS/VLCAppCoordinator.m           |  2 ++
 .../Controller/SettingsController.swift       | 16 ---------
 Sources/Settings/View/SettingsCell.swift      |  1 +
 4 files changed, 35 insertions(+), 19 deletions(-)

diff --git a/Sources/App/iOS/DefaultsChangeListener.swift b/Sources/App/iOS/DefaultsChangeListener.swift
index cc0260f40..e4da29960 100644
--- a/Sources/App/iOS/DefaultsChangeListener.swift
+++ b/Sources/App/iOS/DefaultsChangeListener.swift
@@ -16,6 +16,10 @@ final class DefaultsChangeListener: NSObject {
     private let appTheme: ChangeManager<Int>
     private let appThemeBlack: ChangeManager<Int>
     private let disableGrouping: ChangeManager<Bool>
+    private let hideMediaLibrary: ChangeManager<Bool>
+    private let excludeFromDeviceBackup: ChangeManager<Bool>
+
+    @objc var mediaLibraryService: MediaLibraryService?
 
     override init() {
         let notificationCenter = NotificationCenter.default
@@ -32,18 +36,32 @@ final class DefaultsChangeListener: NSObject {
             notificationCenter.post(name: .VLCDisableGroupingDidChangeNotification, object: nil)
         })
 
+        hideMediaLibrary = ChangeManager(value: Self.readHideMediaLibrary())
+
+        excludeFromDeviceBackup = ChangeManager(value: Self.readExcludeFromDeviceBackup())
+
         super.init()
 
         notificationCenter.addObserver(self,
                                        selector: #selector(userDefaultsUpdated),
                                        name: UserDefaults.didChangeNotification,
                                        object: nil)
+
+        hideMediaLibrary.action = { [weak self] hide in
+            self?.mediaLibraryService?.hideMediaLibrary(hide)
+        }
+
+        excludeFromDeviceBackup.action = { [weak self] exclude in
+            self?.mediaLibraryService?.excludeFromDeviceBackup(exclude)
+        }
     }
 
     @objc private func userDefaultsUpdated() {
         appTheme.update(Self.readAppTheme())
         appThemeBlack.update(Self.readAppThemeBlack())
         disableGrouping.update(Self.readDisableGrouping())
+        hideMediaLibrary.update(Self.readHideMediaLibrary())
+        excludeFromDeviceBackup.update(Self.readExcludeFromDeviceBackup())
     }
 
     static func readAppTheme() -> Int {
@@ -57,14 +75,25 @@ final class DefaultsChangeListener: NSObject {
     static func readDisableGrouping() -> Bool {
         return UserDefaults.standard.bool(forKey: kVLCSettingsDisableGrouping)
     }
+
+    static func readHideMediaLibrary() -> Bool {
+        return UserDefaults.standard.bool(forKey: kVLCSettingHideLibraryInFilesApp)
+    }
+
+    static func readExcludeFromDeviceBackup() -> Bool {
+        // The user interface setting is for *including* in a backup, but
+        // user defaults and the media library refer to *exclusion*. We perform
+        // the inversion right here to have everything match up.
+        return !UserDefaults.standard.bool(forKey: kVLCSettingBackupMediaLibrary)
+    }
 }
 
 /// Executes an action when the underlying value has changed.
 fileprivate final class ChangeManager<T: Equatable> {
     var value: T
-    let action: (T) -> Void
+    var action: ((T) -> Void)?
 
-    init(value: T, action: @escaping (T) -> Void) {
+    init(value: T, action: ((T) -> Void)? = nil) {
         self.value = value
         self.action = action
     }
@@ -72,6 +101,6 @@ fileprivate final class ChangeManager<T: Equatable> {
     func update(_ newValue: T) {
         guard value != newValue else { return }
         value = newValue
-        action(newValue)
+        action?(newValue)
     }
 }
diff --git a/Sources/App/iOS/VLCAppCoordinator.m b/Sources/App/iOS/VLCAppCoordinator.m
index 14c69ed31..9a0f8906d 100644
--- a/Sources/App/iOS/VLCAppCoordinator.m
+++ b/Sources/App/iOS/VLCAppCoordinator.m
@@ -74,6 +74,8 @@
 
     // start the remote control service
     _remoteControlService = [[VLCRemoteControlService alloc] init];
+
+    _defaultsChangeListener.mediaLibraryService = self.mediaLibraryService;
 }
 
 - (MediaLibraryService *)mediaLibraryService
diff --git a/Sources/Settings/Controller/SettingsController.swift b/Sources/Settings/Controller/SettingsController.swift
index 4435b1f2a..287bd6baf 100644
--- a/Sources/Settings/Controller/SettingsController.swift
+++ b/Sources/Settings/Controller/SettingsController.swift
@@ -439,10 +439,6 @@ extension SettingsController: SettingsCellDelegate {
         switch preferenceKey {
         case kVLCSettingPasscodeOnKey:
             passcodeLockSwitchOn(state: isOn)
-        case kVLCSettingHideLibraryInFilesApp:
-            medialibraryHidingLockSwitchOn(state: isOn)
-        case kVLCSettingBackupMediaLibrary:
-            mediaLibraryBackupActivateSwitchOn(state: isOn)
         default:
             break
         }
@@ -493,18 +489,6 @@ extension SettingsController {
     }
 }
 
-extension SettingsController {
-    func medialibraryHidingLockSwitchOn(state: Bool) {
-        mediaLibraryService.hideMediaLibrary(state)
-    }
-}
-
-extension SettingsController {
-    func mediaLibraryBackupActivateSwitchOn(state: Bool) {
-        mediaLibraryService.excludeFromDeviceBackup(state)
-    }
-}
-
 extension SettingsController: ActionSheetSpecifierDelegate {
     func actionSheetSpecifierHandleToggleSwitch(for cell: ActionSheetCell, state: Bool) {
         switch cell.identifier {
diff --git a/Sources/Settings/View/SettingsCell.swift b/Sources/Settings/View/SettingsCell.swift
index 7d83ea10d..a529bacec 100644
--- a/Sources/Settings/View/SettingsCell.swift
+++ b/Sources/Settings/View/SettingsCell.swift
@@ -15,6 +15,7 @@ protocol SettingsCellDelegate: AnyObject {
     /// Implementations should only perform side effects on
     /// specific preferences; updating the preference itself
     /// is handled by the cell.
+    @available(*, deprecated, message: "Side effects of toggling a setting should be performed by observing changes to userdefaults")
     func settingsCellDidChangeSwitchState(cell: SettingsCell, preferenceKey: String, isOn: Bool)
 
     func settingsCellInfoButtonPressed(cell: SettingsCell, preferenceKey: String)
-- 
GitLab