kiwix-apple/Views/Settings/LanguageSelector.swift
2024-08-05 11:54:37 +00:00

202 lines
7.7 KiB
Swift

// This file is part of Kiwix for iOS & macOS.
//
// Kiwix is free software; you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by
// the Free Software Foundation; either version 3 of the License, or
// any later version.
//
// Kiwix is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Kiwix; If not, see https://www.gnu.org/licenses/.
import CoreData
import SwiftUI
import Defaults
#if os(macOS)
struct LanguageSelector: View {
@Default(.libraryLanguageCodes) private var selected
@EnvironmentObject private var library: LibraryViewModel
@State private var languages = [Language]()
@State private var sortOrder = [KeyPathComparator(\Language.count, order: .reverse)]
var body: some View {
Table(languages, sortOrder: $sortOrder) {
TableColumn("") { language in
Toggle("", isOn: Binding {
selected.contains(language.code)
} set: { isSelected in
if isSelected {
selected.insert(language.code)
} else if selected.count > 1 {
selected.remove(language.code)
}
})
}.width(14)
TableColumn("language_selector.name.title".localized, value: \.name)
TableColumn("language_selector.count.table.title".localized, value: \.count) { language in
Text(language.count.formatted())
}
}
.opacity( library.state == .complete ? 1.0 : 0.3)
.tableStyle(.bordered(alternatesRowBackgrounds: true))
.onChange(of: sortOrder) { languages.sort(using: $0) }
.onChange(of: library.state) { state in
guard state != .inProgress else { return }
reloadLanguages()
}
.onAppear {
reloadLanguages()
}
}
private func reloadLanguages() {
Task {
languages = await Languages.fetch()
languages.sort(using: sortOrder)
}
}
}
#elseif os(iOS)
struct LanguageSelector: View {
@Default(.libraryLanguageSortingMode) private var sortingMode
@State private var showing = [Language]()
@State private var hiding = [Language]()
var body: some View {
List {
Section {
if showing.isEmpty {
Text("language_selector.no_language.title".localized).foregroundColor(.secondary)
} else {
ForEach(showing) { language in
Button { hide(language) } label: { LanguageLabel(language: language) }
}
}
} header: { Text("language_selector.showing.header".localized) }
Section {
ForEach(hiding) { language in
Button { show(language) } label: { LanguageLabel(language: language) }
}
} header: { Text("language_selector.hiding.header".localized) }
}
.listStyle(.insetGrouped)
.navigationTitle("language_selector.navitation.title".localized)
.navigationBarTitleDisplayMode(.inline)
.toolbar {
Picker(selection: $sortingMode) {
ForEach(LibraryLanguageSortingMode.allCases) { sortingMode in
Text(sortingMode.name).tag(sortingMode)
}
} label: {
Label("language_selector.toolbar.sorting".localized, systemImage: "arrow.up.arrow.down")
}.pickerStyle(.menu)
}
.onAppear {
Task {
var languages = await Languages.fetch()
languages.sort(by: Languages.compare(lhs:rhs:))
showing = languages.filter { Defaults[.libraryLanguageCodes].contains($0.code) }
hiding = languages.filter { !Defaults[.libraryLanguageCodes].contains($0.code) }
}
}
.onChange(of: sortingMode) { _ in
showing.sort(by: Languages.compare(lhs:rhs:))
hiding.sort(by: Languages.compare(lhs:rhs:))
}
}
private func show(_ language: Language) {
Defaults[.libraryLanguageCodes].insert(language.code)
withAnimation {
hiding.removeAll { $0.code == language.code }
showing.append(language)
showing.sort(by: Languages.compare(lhs:rhs:))
}
}
private func hide(_ language: Language) {
guard Defaults[.libraryLanguageCodes].count > 1 else {
// we should not remove all languages, it will produce empty results
return
}
Defaults[.libraryLanguageCodes].remove(language.code)
withAnimation {
showing.removeAll { $0.code == language.code }
hiding.append(language)
hiding.sort(by: Languages.compare(lhs:rhs:))
}
}
}
private struct LanguageLabel: View {
let language: Language
var body: some View {
HStack {
Text(language.name).foregroundColor(.primary)
Spacer()
Text("\(language.count)").foregroundColor(.secondary)
}
}
}
#endif
class Languages {
/// Retrieve a list of languages.
/// - Returns: languages with count of zim files in each language
static func fetch() async -> [Language] {
let count = NSExpressionDescription()
count.name = "count"
count.expression = NSExpression(forFunction: "count:", arguments: [NSExpression(forKeyPath: "languageCode")])
count.expressionResultType = .integer16AttributeType
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: "ZimFile")
// exclude the already downloaded files, they might have invalid language set
// but we are mainly interested in fetched content
fetchRequest.predicate = ZimFile.Predicate.notDownloaded
fetchRequest.propertiesToFetch = ["languageCode", count]
fetchRequest.propertiesToGroupBy = ["languageCode"]
fetchRequest.resultType = .dictionaryResultType
let languages: [Language] = await withCheckedContinuation { continuation in
Database.shared.performBackgroundTask { context in
guard let results = try? context.fetch(fetchRequest) else {
continuation.resume(returning: [])
return
}
let collector = LanguageCollector()
for result in results {
if let result = result as? NSDictionary,
let languageCodes = result["languageCode"] as? String,
let count = result["count"] as? Int {
collector.addLanguages(codes: languageCodes, count: count)
}
}
continuation.resume(returning: collector.languages())
}
}
return languages
}
/// Compare two languages based on library language sorting order.
/// Can be removed once support for iOS 14 drops.
/// - Parameters:
/// - lhs: one language to compare
/// - rhs: another language to compare
/// - Returns: if one language should appear before or after another
static func compare(lhs: Language, rhs: Language) -> Bool {
switch Defaults[.libraryLanguageSortingMode] {
case .alphabetically:
return lhs.name.caseInsensitiveCompare(rhs.name) == .orderedAscending
case .byCounts:
return lhs.count > rhs.count
}
}
}