Merge pull request #1240 from omnivore-app/fix/ios-voice-handling

Refactor voices code, show an indicator when switching voices
This commit is contained in:
Jackson Harper 2022-09-27 09:43:53 +08:00 committed by GitHub
commit 38236e6199
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
8 changed files with 182 additions and 88 deletions

View file

@ -22,7 +22,7 @@ struct TextToSpeechLanguageView: View {
}
private var innerBody: some View {
ForEach(VOICELANGUAGES, id: \.key.self) { language in
ForEach(Voices.Languages, id: \.key.self) { language in
Button(action: {
audioController.defaultLanguage = language.key
}) {
@ -32,7 +32,11 @@ struct TextToSpeechLanguageView: View {
Spacer()
if audioController.defaultLanguage == language.key {
Image(systemName: "checkmark")
if audioController.isPlaying, audioController.isLoading {
ProgressView()
} else {
Image(systemName: "checkmark")
}
}
}
.contentShape(Rectangle())

View file

@ -29,7 +29,7 @@ struct TextToSpeechView: View {
private var innerBody: some View {
Section("Voices") {
ForEach(VOICELANGUAGES, id: \.key) { language in
ForEach(Voices.Languages, id: \.key) { language in
NavigationLink(destination: TextToSpeechVoiceSelectionView(forLanguage: language)) {
Text(language.name)
}

View file

@ -0,0 +1,72 @@
import Models
import Services
import SwiftUI
import Views
struct TextToSpeechVoiceSelectionView: View {
@EnvironmentObject var audioController: AudioController
let language: VoiceLanguage
init(forLanguage: VoiceLanguage) {
self.language = forLanguage
}
var body: some View {
Group {
#if os(iOS)
Form {
Section("Language") {
NavigationLink(destination: TextToSpeechLanguageView().navigationTitle("Language")) {
Text(audioController.currentVoiceLanguage.name)
}
}
innerBody
}
#elseif os(macOS)
List {
innerBody
}
.listStyle(InsetListStyle())
#endif
}
.navigationTitle("Choose a Voice")
}
private var innerBody: some View {
ForEach(language.categories, id: \.self) { category in
Section(category.rawValue) {
ForEach(audioController.voiceList?.filter { $0.category == category } ?? [], id: \.key.self) { voice in
HStack {
// Voice samples are not working yet
// Button(action: {
// audioController.playVoiceSample(voice: voice.key)
// }) {
// Image(systemName: "play.circle").font(.appTitleTwo)
// }
// .buttonStyle(PlainButtonStyle())
Button(action: {
audioController.setPreferredVoice(voice.key, forLanguage: language.key)
audioController.currentVoice = voice.key
}) {
HStack {
Text(voice.name)
Spacer()
if voice.selected {
if audioController.isPlaying, audioController.isLoading {
ProgressView()
} else {
Image(systemName: "checkmark")
}
}
}
.contentShape(Rectangle())
}
.buttonStyle(PlainButtonStyle())
}
}
}
}
}
}

View file

@ -63,7 +63,13 @@ public extension LinkedItem {
}
var publisherDisplayName: String? {
siteName ?? URL(string: publisherURLString ?? pageURLString ?? "")?.host
if let siteName = siteName {
return siteName
}
if let host = URL(string: publisherURLString ?? pageURLString ?? "")?.host, host != "storage.googleapis.com" {
return host
}
return nil
}
var imageURL: URL? {

View file

@ -32,80 +32,6 @@ enum DownloadPriority: String {
case high
}
public struct VoiceLanguage {
public let key: String
public let name: String
public let defaultVoice: String
public let categories: [VoiceCategory]
}
public enum VoiceCategory: String, CaseIterable {
case enUS = "English (US)"
case enAU = "English (Australia)"
case enCA = "English (Canada)"
case enIE = "English (Ireland)"
case enIN = "English (India)"
case enSG = "English (Singapore)"
case enUK = "English (UK)"
case deDE = "German (Germany)"
case esES = "Spanish (Spain)"
case jaJP = "Japanese (Japan)"
case zhCN = "Chinese (China Mainland)"
}
public struct VoicePair {
let firstKey: String
let secondKey: String
let firstName: String
let secondName: String
let language: String
let category: VoiceCategory
}
private let ENGLISH = VoiceLanguage(key: "en",
name: "English",
defaultVoice: "en-US-ChristopherNeural",
categories: [.enUS, .enAU, .enCA, .enIE, .enIN, .enSG, .enUK])
public let VOICELANGUAGES = [
ENGLISH,
VoiceLanguage(key: "zh", name: "Chinese", defaultVoice: "zh-CN-XiaochenNeural", categories: [.zhCN]),
VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]),
VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]),
VoiceLanguage(key: "de", name: "German", defaultVoice: "de-CH-JanNeural", categories: [.deDE]),
VoiceLanguage(key: "es", name: "Spanish", defaultVoice: "es-ES-AlvaroNeural", categories: [.esES])
]
// swiftlint:disable all
public let VOICES = [
// en
VoicePair(firstKey: "en-US-JennyNeural", secondKey: "en-US-BrandonNeural", firstName: "Jenny", secondName: "Brandon", language: "en-US", category: .enUS),
VoicePair(firstKey: "en-US-CoraNeural", secondKey: "en-US-ChristopherNeural", firstName: "Cora", secondName: "Christopher", language: "en-US", category: .enUS),
VoicePair(firstKey: "en-US-ElizabethNeural", secondKey: "en-US-EricNeural", firstName: "Elizabeth", secondName: "Eric", language: "en-US", category: .enUS),
VoicePair(firstKey: "en-CA-ClaraNeural", secondKey: "en-CA-LiamNeural", firstName: "Clara", secondName: "Liam", language: "en-CA", category: .enCA),
VoicePair(firstKey: "en-GB-LibbyNeural", secondKey: "en-GB-EthanNeural", firstName: "Libby", secondName: "Ethan", language: "en-GB", category: .enUK),
VoicePair(firstKey: "en-AU-NatashaNeural", secondKey: "en-AU-WilliamNeural", firstName: "Natasha", secondName: "William", language: "en-AU", category: .enAU),
VoicePair(firstKey: "en-IE-ConnorNeural", secondKey: "en-IE-EmilyNeural", firstName: "Connor", secondName: "Emily", language: "en-IE", category: .enIE),
VoicePair(firstKey: "en-IN-NeerjaNeural", secondKey: "en-IN-PrabhatNeural", firstName: "Neerja", secondName: "Prabhat", language: "en-IN", category: .enIN),
VoicePair(firstKey: "en-SG-LunaNeural", secondKey: "en-SG-WayneNeural", firstName: "Luna", secondName: "Wayne", language: "en-SG", category: .enSG),
VoicePair(firstKey: "es-ES-AlvaroNeural", secondKey: "es-ES-ElviraNeural", firstName: "Alvaro", secondName: "Elvira", language: "es-ES", category: .esES),
VoicePair(firstKey: "de-CH-LeniNeural", secondKey: "de-DE-KatjaNeural", firstName: "Leni", secondName: "Katja", language: "de-DE", category: .deDE),
VoicePair(firstKey: "de-DE-AmalaNeural", secondKey: "de-DE-BerndNeural", firstName: "Amala", secondName: "Bernd", language: "de-DE", category: .deDE),
VoicePair(firstKey: "de-DE-ChristophNeural", secondKey: "de-DE-LouisaNeural", firstName: "Christoph", secondName: "Louisa", language: "de-DE", category: .deDE),
// ja
VoicePair(firstKey: "ja-JP-NanamiNeural", secondKey: "ja-JP-KeitaNeural", firstName: "Nanami", secondName: "Keita", language: "ja-JP", category: .jaJP),
// zh
VoicePair(firstKey: "zh-CN-XiaochenNeural", secondKey: "zh-CN-XiaohanNeural", firstName: "Xiaochen", secondName: "Xiaohan", language: "zh-CN", category: .zhCN),
VoicePair(firstKey: "zh-CN-XiaoxiaoNeural", secondKey: "zh-CN-YunyangNeural", firstName: "Xiaoxiao", secondName: "Yunyang", language: "zh-CN", category: .zhCN)
]
let VOICE_REGIONS = ["English "]
// Somewhat based on: https://github.com/neekeetab/CachingPlayerItem/blob/master/CachingPlayerItem.swift
class SpeechPlayerItem: AVPlayerItem {
let resourceLoaderDelegate = ResourceLoaderDelegate()
@ -260,6 +186,7 @@ class SpeechPlayerItem: AVPlayerItem {
}
}
// swiftlint:disable all
public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate {
@Published public var state: AudioControllerState = .stopped
@Published public var itemAudioProperties: LinkedItemAudioProperties?
@ -328,7 +255,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
}
public func generateVoiceList() -> [(name: String, key: String, category: VoiceCategory, selected: Bool)] {
VOICES.flatMap { voicePair in
Voices.Pairs.flatMap { voicePair in
[
(name: voicePair.firstName, key: voicePair.firstKey, category: voicePair.category, selected: voicePair.firstKey == currentVoice),
(name: voicePair.secondName, key: voicePair.secondKey, category: voicePair.category, selected: voicePair.secondKey == currentVoice)
@ -466,7 +393,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
@AppStorage(UserDefaultKey.textToSpeechPreloadEnabled.rawValue) public var preloadEnabled = true
public var currentVoiceLanguage: VoiceLanguage {
VOICELANGUAGES.first(where: { $0.key == currentLanguage }) ?? ENGLISH
Voices.Languages.first(where: { $0.key == currentLanguage }) ?? Voices.English
}
private var _currentLanguage: String?
@ -475,7 +402,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
if let currentLanguage = _currentLanguage {
return currentLanguage
}
if let itemLang = itemAudioProperties?.language, let lang = VOICELANGUAGES.first(where: { $0.name == itemLang || $0.key == itemLang }) {
if let itemLang = itemAudioProperties?.language, let lang = Voices.Languages.first(where: { $0.name == itemLang || $0.key == itemLang }) {
return lang.key
}
return defaultLanguage
@ -519,7 +446,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
public var currentVoicePair: VoicePair? {
let voice = currentVoice
return VOICES.first(where: { $0.firstKey == voice || $0.secondKey == voice })
return Voices.Pairs.first(where: { $0.firstKey == voice || $0.secondKey == voice })
}
public func getPreferredVoice(forLanguage language: String) -> String {
@ -559,7 +486,7 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
}
public var secondaryVoice: String {
let pair = VOICES.first { $0.firstKey == currentVoice || $0.secondKey == currentVoice }
let pair = Voices.Pairs.first { $0.firstKey == currentVoice || $0.secondKey == currentVoice }
if let pair = pair {
if pair.firstKey == currentVoice {
return pair.secondKey
@ -591,16 +518,26 @@ public class AudioController: NSObject, ObservableObject, AVAudioPlayerDelegate
}
}
public var isLoading: Bool {
if state == .reachedEnd {
return false
}
return (state == .loading || player?.currentItem == nil || player?.currentItem?.status == .unknown)
}
public var isPlaying: Bool {
state == .playing
}
public func isLoadingItem(itemID: String) -> Bool {
if state == .reachedEnd {
return false
}
return itemAudioProperties?.itemID == itemID &&
(state == .loading || player?.currentItem == nil || player?.currentItem?.status == .unknown)
return itemAudioProperties?.itemID == itemID && isLoading
}
public func isPlayingItem(itemID: String) -> Bool {
state == .playing && itemAudioProperties?.itemID == itemID
itemAudioProperties?.itemID == itemID && isPlaying
}
public func skipForward(seconds: Double) {

View file

@ -0,0 +1,75 @@
//
// Voices.swift
//
//
// Created by Jackson Harper on 9/26/22.
//
import Foundation
public struct VoiceLanguage {
public let key: String
public let name: String
public let defaultVoice: String
public let categories: [VoiceCategory]
}
public enum VoiceCategory: String, CaseIterable {
case enUS = "English (US)"
case enAU = "English (Australia)"
case enCA = "English (Canada)"
case enIE = "English (Ireland)"
case enIN = "English (India)"
case enSG = "English (Singapore)"
case enUK = "English (UK)"
case deDE = "German (Germany)"
case esES = "Spanish (Spain)"
case jaJP = "Japanese (Japan)"
case zhCN = "Chinese (China Mainland)"
}
public struct VoicePair {
let firstKey: String
let secondKey: String
let firstName: String
let secondName: String
let language: String
let category: VoiceCategory
}
public enum Voices {
public static let English = VoiceLanguage(key: "en",
name: "English",
defaultVoice: "en-US-ChristopherNeural",
categories: [.enUS, .enAU, .enCA, .enIE, .enIN, .enSG, .enUK])
public static let Languages = [
English,
VoiceLanguage(key: "zh", name: "Chinese", defaultVoice: "zh-CN-XiaochenNeural", categories: [.zhCN]),
VoiceLanguage(key: "de", name: "German", defaultVoice: "de-CH-JanNeural", categories: [.deDE]),
VoiceLanguage(key: "ja", name: "Japanese", defaultVoice: "ja-JP-NanamiNeural", categories: [.jaJP]),
VoiceLanguage(key: "es", name: "Spanish", defaultVoice: "es-ES-AlvaroNeural", categories: [.esES])
]
// swiftlint:disable all
public static let Pairs = [
VoicePair(firstKey: "en-US-JennyNeural", secondKey: "en-US-BrandonNeural", firstName: "Jenny", secondName: "Brandon", language: "en-US", category: .enUS),
VoicePair(firstKey: "en-US-CoraNeural", secondKey: "en-US-ChristopherNeural", firstName: "Cora", secondName: "Christopher", language: "en-US", category: .enUS),
VoicePair(firstKey: "en-US-ElizabethNeural", secondKey: "en-US-EricNeural", firstName: "Elizabeth", secondName: "Eric", language: "en-US", category: .enUS),
VoicePair(firstKey: "en-CA-ClaraNeural", secondKey: "en-CA-LiamNeural", firstName: "Clara", secondName: "Liam", language: "en-CA", category: .enCA),
VoicePair(firstKey: "en-GB-LibbyNeural", secondKey: "en-GB-EthanNeural", firstName: "Libby", secondName: "Ethan", language: "en-GB", category: .enUK),
VoicePair(firstKey: "en-AU-NatashaNeural", secondKey: "en-AU-WilliamNeural", firstName: "Natasha", secondName: "William", language: "en-AU", category: .enAU),
VoicePair(firstKey: "en-IE-ConnorNeural", secondKey: "en-IE-EmilyNeural", firstName: "Connor", secondName: "Emily", language: "en-IE", category: .enIE),
VoicePair(firstKey: "en-IN-NeerjaNeural", secondKey: "en-IN-PrabhatNeural", firstName: "Neerja", secondName: "Prabhat", language: "en-IN", category: .enIN),
VoicePair(firstKey: "en-SG-LunaNeural", secondKey: "en-SG-WayneNeural", firstName: "Luna", secondName: "Wayne", language: "en-SG", category: .enSG),
VoicePair(firstKey: "zh-CN-XiaochenNeural", secondKey: "zh-CN-XiaohanNeural", firstName: "Xiaochen", secondName: "Xiaohan", language: "zh-CN", category: .zhCN),
VoicePair(firstKey: "zh-CN-XiaoxiaoNeural", secondKey: "zh-CN-YunyangNeural", firstName: "Xiaoxiao", secondName: "Yunyang", language: "zh-CN", category: .zhCN),
VoicePair(firstKey: "es-ES-AlvaroNeural", secondKey: "es-ES-ElviraNeural", firstName: "Alvaro", secondName: "Elvira", language: "es-ES", category: .esES),
VoicePair(firstKey: "de-CH-LeniNeural", secondKey: "de-DE-KatjaNeural", firstName: "Leni", secondName: "Katja", language: "de-DE", category: .deDE),
VoicePair(firstKey: "de-DE-AmalaNeural", secondKey: "de-DE-BerndNeural", firstName: "Amala", secondName: "Bernd", language: "de-DE", category: .deDE),
VoicePair(firstKey: "de-DE-ChristophNeural", secondKey: "de-DE-LouisaNeural", firstName: "Christoph", secondName: "Louisa", language: "de-DE", category: .deDE),
VoicePair(firstKey: "ja-JP-NanamiNeural", secondKey: "ja-JP-KeitaNeural", firstName: "Nanami", secondName: "Keita", language: "ja-JP", category: .jaJP)
]
}

View file

@ -188,7 +188,7 @@ public final class DataService: ObservableObject {
do {
try self.backgroundContext.save()
logger.debug("ArticleContent saved succesfully")
logger.debug("local ArticleContent saved succesfully")
objectID = linkedItem.objectID
} catch {
self.backgroundContext.rollback()

File diff suppressed because one or more lines are too long