Feature: user-selectable theme (#460)

* Fix #457 - Persist entry sorting preferences across app launches
Changed sorting state from @State to @AppStorage in EntriesView to ensure user preferences are saved to UserDefaults. Also made sorting criteria mutually exclusive for a better UX.

* Add option to pick theme (light/dark/auto).
Moved language files and other resources into a sub-folder to clean up the structure a little.

* use #EnvironmentObject
This commit is contained in:
Ben Hughes
2026-02-26 07:28:46 +11:00
committed by GitHub
parent a2f9e8e7a2
commit 46b52d3c75
70 changed files with 263 additions and 25 deletions
+10
View File
@@ -4,18 +4,28 @@ import SharedLib
final class AppSetting: ObservableObject {
@Published var webFontSizePercent: Double
@Published var theme: Theme
private var cancellable = Set<AnyCancellable>()
init() {
webFontSizePercent = WallabagUserDefaults.webFontSizePercent
theme = Theme(rawValue: WallabagUserDefaults.theme) ?? .auto
$webFontSizePercent
.sink(receiveValue: updateWebFontSizePercent)
.store(in: &cancellable)
$theme
.sink(receiveValue: updateTheme)
.store(in: &cancellable)
}
private func updateWebFontSizePercent(_ value: Double) {
WallabagUserDefaults.webFontSizePercent = value
}
private func updateTheme(_ value: Theme) {
WallabagUserDefaults.theme = value.rawValue
}
}
+10
View File
@@ -1,3 +1,4 @@
import Factory
import SharedLib
import SwiftUI
@@ -7,9 +8,17 @@ struct SettingView: View {
@AppStorage("badge") var badge: Bool = true
@AppStorage("defaultMode") var defaultMode: String = RetrieveMode.allArticles.rawValue
@AppStorage("itemPerPageDuringSync") var itemPerPageDuringSync: Int = 50
@EnvironmentObject var appSetting: AppSetting
var body: some View {
Form {
Section("Appearance") {
Picker("Theme", selection: $appSetting.theme) {
ForEach(Theme.allCases) { theme in
Text(theme.name).tag(theme)
}
}
}
Section("Entries list") {
Toggle("Show image in list", isOn: $showImageInList)
Picker("Default mode", selection: $defaultMode) {
@@ -35,5 +44,6 @@ struct SettingView: View {
struct SettingView_Previews: PreviewProvider {
static var previews: some View {
SettingView()
.environmentObject(AppSetting())
}
}
+25
View File
@@ -0,0 +1,25 @@
import SwiftUI
enum Theme: String, CaseIterable, Identifiable {
case auto
case light
case dark
var id: String { rawValue }
var colorScheme: ColorScheme? {
switch self {
case .auto: return nil
case .light: return .light
case .dark: return .dark
}
}
var name: LocalizedStringKey {
switch self {
case .auto: return "Auto"
case .light: return "Light"
case .dark: return "Dark"
}
}
}

Before

Width:  |  Height:  |  Size: 56 KiB

After

Width:  |  Height:  |  Size: 56 KiB

Before

Width:  |  Height:  |  Size: 249 KiB

After

Width:  |  Height:  |  Size: 249 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 3.8 KiB

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 32 KiB

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 4.7 KiB

Before

Width:  |  Height:  |  Size: 87 KiB

After

Width:  |  Height:  |  Size: 87 KiB

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.8 KiB

Before

Width:  |  Height:  |  Size: 14 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

@@ -0,0 +1,7 @@
// Theme
"Appearance" = "المظهر";
"Theme" = "السمة";
"Auto" = "تلقائي";
"Light" = "فاتح";
"Dark" = "داكن";
@@ -17,3 +17,10 @@
// Menu
"Menu" = "Nabídka";
// Theme
"Appearance" = "Vzhled";
"Theme" = "Motiv";
"Auto" = "Automaticky";
"Light" = "Světlý";
"Dark" = "Tmavý";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Udseende";
"Theme" = "Tema";
"Auto" = "Auto";
"Light" = "Lyst";
"Dark" = "Mørkt";
@@ -48,3 +48,10 @@
//Player
"Select one entry" = "Eintrag auswählen";
// Theme
"Appearance" = "Erscheinungsbild";
"Theme" = "Design";
"Auto" = "Automatisch";
"Light" = "Hell";
"Dark" = "Dunkel";
@@ -50,3 +50,10 @@
//Player
"Select one entry" = "Select one entry";
// Theme
"Appearance" = "Appearance";
"Theme" = "Theme";
"Auto" = "Auto";
"Light" = "Light";
"Dark" = "Dark";
@@ -1,10 +1,13 @@
// Tip View
"This application is developed on free time" = "Esta aplicación se desarrolla en tiempo libre.";
"Logout" = "Desconectarse";
"Select one entry" = "Selecciona una entrada";
// Theme
"Appearance" = "Apariencia";
"Theme" = "Tema";
"Auto" = "Automático";
"Light" = "Claro";
"Dark" = "Oscuro";
"Setting" = "Configuración";
"About" = "Acerca de";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "ظاهر";
"Theme" = "پوسته";
"Auto" = "خودکار";
"Light" = "روشن";
"Dark" = "تیره";
@@ -54,3 +54,10 @@
"Order by id" = "Trier par id";
"Order by reading time" = "Trier par temps de lecture";
"Sorting" = "Tri";
// Theme
"Appearance" = "Apparence";
"Theme" = "Thème";
"Auto" = "Auto";
"Light" = "Clair";
"Dark" = "Sombre";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Aparencia";
"Theme" = "Tema";
"Auto" = "Auto";
"Light" = "Claro";
"Dark" = "Escuro";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "दिखावट";
"Theme" = "थीम";
"Auto" = "ऑटो";
"Light" = "हल्का";
"Dark" = "गहरा";
@@ -48,3 +48,10 @@
//Player
"Select one entry" = "Odaberi unos";
// Theme
"Appearance" = "Izgled";
"Theme" = "Tema";
"Auto" = "Automatski";
"Light" = "Svijetlo";
"Dark" = "Tamno";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Megjelenés";
"Theme" = "Téma";
"Auto" = "Automatikus";
"Light" = "Világos";
"Dark" = "Sötét";
@@ -33,3 +33,10 @@
// Menu
"Menu" = "Menù";
// Theme
"Appearance" = "Aspetto";
"Theme" = "Tema";
"Auto" = "Automatico";
"Light" = "Chiaro";
"Dark" = "Scuro";
@@ -42,3 +42,10 @@
"Loading..." = "読み込み中...";
"Don" = "寄付";
"Starred" = "スター";
// Theme
"Appearance" = "外観";
"Theme" = "テーマ";
"Auto" = "自動";
"Light" = "ライト";
"Dark" = "ダーク";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "모양";
"Theme" = "테마";
"Auto" = "자동";
"Light" = "라이트";
"Dark" = "다크";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Utseende";
"Theme" = "Tema";
"Auto" = "Auto";
"Light" = "Lyst";
"Dark" = "Mørkt";
@@ -33,3 +33,10 @@
"Loading..." = "Laden...";
"But you can contribute financially by making a donation whenever you want to support the project." = "Maar u kunt ook financieel bijdragen door een donatie te doen u het project wilt steunen.";
"Entries" = "Items";
// Theme
"Appearance" = "Weergave";
"Theme" = "Thema";
"Auto" = "Automatisch";
"Light" = "Licht";
"Dark" = "Donker";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Aparéncia";
"Theme" = "Tèma";
"Auto" = "Auto";
"Light" = "Clar";
"Dark" = "Escur";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Wygląd";
"Theme" = "Motyw";
"Auto" = "Automatyczny";
"Light" = "Jasny";
"Dark" = "Ciemny";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Aparência";
"Theme" = "Tema";
"Auto" = "Automático";
"Light" = "Claro";
"Dark" = "Escuro";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Aspect";
"Theme" = "Temă";
"Auto" = "Auto";
"Light" = "Luminos";
"Dark" = "Întunecat";
@@ -40,3 +40,10 @@
// Search
"Search" = "Поиск";
// Theme
"Appearance" = "Оформление";
"Theme" = "Тема";
"Auto" = "Автоматически";
"Light" = "Светлая";
"Dark" = "Темная";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Utseende";
"Theme" = "Tema";
"Auto" = "Auto";
"Light" = "Ljust";
"Dark" = "Mörkt";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "ลักษณะ";
"Theme" = "ธีม";
"Auto" = "อัตโนมัติ";
"Light" = "สว่าง";
"Dark" = "มืด";
@@ -46,3 +46,10 @@
//Player
"Select one entry" = "Bir makale seçin";
// Theme
"Appearance" = "Görünüm";
"Theme" = "Tema";
"Auto" = "Otomatik";
"Light" = "Açık";
"Dark" = "Koyu";
@@ -0,0 +1,7 @@
// Theme
"Appearance" = "Вигляд";
"Theme" = "Тема";
"Auto" = "Авто";
"Light" = "Світла";
"Dark" = "Темна";
@@ -48,3 +48,10 @@
//Player
"Select one entry" = "选择一个条目";
// Theme
"Appearance" = "外观";
"Theme" = "主题";
"Auto" = "自动";
"Light" = "浅色";
"Dark" = "深色";
@@ -42,3 +42,10 @@
// Menu
"Menu" = "菜單";
// Theme
"Appearance" = "外觀";
"Theme" = "主題";
"Auto" = "自動";
"Light" = "淺色";
"Dark" = "深色";
+1
View File
@@ -39,6 +39,7 @@ struct WallabagApp: App {
.environment(errorHandler)
.environmentObject(appSetting)
.environment(\.managedObjectContext, coreData.viewContext)
.preferredColorScheme(appSetting.theme.colorScheme)
}
.onChange(of: scenePhase) { _, newScenePhase in
if newScenePhase == .active {
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
@@ -48,4 +48,7 @@ public enum WallabagUserDefaults {
@GeneralSetting("itemPerPageDuringSync", defaultValue: 50)
public static var itemPerPageDuringSync: Int
@GeneralSetting("theme", defaultValue: "auto")
public static var theme: String
}
+17 -5
View File
@@ -25,6 +25,7 @@
09644B5725C9810A000FFDA1 /* WallabagApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644B5625C9810A000FFDA1 /* WallabagApp.swift */; };
09644B5E25C98116000FFDA1 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644B5D25C98116000FFDA1 /* AppDelegate.swift */; };
09644B7E25C98152000FFDA1 /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644B7D25C98152000FFDA1 /* AppState.swift */; };
09FA20240000000000000002 /* Theme.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09FA20240000000000000001 /* Theme.swift */; };
09644B8C25C98176000FFDA1 /* Route.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644B8825C98176000FFDA1 /* Route.swift */; };
09644B9725C9819F000FFDA1 /* PlayerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644B9425C9819F000FFDA1 /* PlayerView.swift */; };
09644B9825C9819F000FFDA1 /* PlayerPublisher.swift in Sources */ = {isa = PBXBuildFile; fileRef = 09644B9525C9819F000FFDA1 /* PlayerPublisher.swift */; };
@@ -171,6 +172,7 @@
09644B5625C9810A000FFDA1 /* WallabagApp.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = WallabagApp.swift; sourceTree = "<group>"; };
09644B5D25C98116000FFDA1 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = "<group>"; };
09644B7D25C98152000FFDA1 /* AppState.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
09FA20240000000000000001 /* Theme.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Theme.swift; sourceTree = "<group>"; };
09644B8825C98176000FFDA1 /* Route.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Route.swift; sourceTree = "<group>"; };
09644B9425C9819F000FFDA1 /* PlayerView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerView.swift; sourceTree = "<group>"; };
09644B9525C9819F000FFDA1 /* PlayerPublisher.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = PlayerPublisher.swift; sourceTree = "<group>"; };
@@ -399,6 +401,7 @@
09644B7D25C98152000FFDA1 /* AppState.swift */,
09644BD125C9833D000FFDA1 /* DependencyInjection.swift */,
09644BD325C9833D000FFDA1 /* WallabagError.swift */,
09FA20240000000000000001 /* Theme.swift */,
);
path = Lib;
sourceTree = "<group>";
@@ -744,6 +747,18 @@
name = Products;
sourceTree = "<group>";
};
09FA20300000000000000001 /* Resources */ = {
isa = PBXGroup;
children = (
09AD759C2624F93D00708A1E /* html-ressources */,
09AD75C02624FEEE00708A1E /* Localizable.strings */,
09644D1E25C98782000FFDA1 /* wallabagStore.xcdatamodeld */,
0951C61729CC2EB000D8E8C6 /* Assets.xcassets */,
099CD12E2B5019F40029E94A /* WallabagStoreKit.storekit */,
);
path = Resources;
sourceTree = "<group>";
};
09BFB28625C8348E00E12B4D /* App */ = {
isa = PBXGroup;
children = (
@@ -753,13 +768,9 @@
097F824C25CB1B17006C85F6 /* Entity */,
09644D1625C9874D000FFDA1 /* Extension */,
09644B8425C98161000FFDA1 /* Features */,
09AD759C2624F93D00708A1E /* html-ressources */,
09644B7C25C9814C000FFDA1 /* Lib */,
09AD75C02624FEEE00708A1E /* Localizable.strings */,
09644BE425C98343000FFDA1 /* PropertyWrapper */,
09644D1E25C98782000FFDA1 /* wallabagStore.xcdatamodeld */,
0951C61729CC2EB000D8E8C6 /* Assets.xcassets */,
099CD12E2B5019F40029E94A /* WallabagStoreKit.storekit */,
09FA20300000000000000001 /* Resources */,
);
path = App;
sourceTree = "<group>";
@@ -1067,6 +1078,7 @@
09644BAF25C98213000FFDA1 /* RefreshButton.swift in Sources */,
09BE0AF42A9F45E900193FBF /* View+Extension.swift in Sources */,
09644C7025C985A9000FFDA1 /* ArchiveEntryButton.swift in Sources */,
09FA20240000000000000002 /* Theme.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};