From b8558ec2f7ced5d952321f9207134996830a2bef Mon Sep 17 00:00:00 2001 From: Jackson Harper Date: Fri, 17 Nov 2023 21:17:43 +0800 Subject: [PATCH] Improve AI voices modal --- .../App/Views/Home/HomeFeedViewIOS.swift | 15 +- .../App/Views/Home/HomeFeedViewModel.swift | 1 - .../App/Views/Home/OpenAIVoicesModal.swift | 143 ++++++++++++++++++ .../App/Views/RootView/RootViewModel.swift | 3 - .../Sources/Utils/UserDefaultKeys.swift | 3 +- .../Sources/Views/CommunityModal.swift | 129 ---------------- 6 files changed, 158 insertions(+), 136 deletions(-) create mode 100644 apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift delete mode 100644 apple/OmnivoreKit/Sources/Views/CommunityModal.swift diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift index d56fe3e74..39aff2c62 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewIOS.swift @@ -32,12 +32,14 @@ struct AnimatingCellHeight: AnimatableModifier { @State var isListScrolled = false @State var listTitle = "" @State var isEditMode: EditMode = .inactive + @State var showOpenAIVoices = false @EnvironmentObject var dataService: DataService @EnvironmentObject var audioController: AudioController @AppStorage(UserDefaultKey.homeFeedlayoutPreference.rawValue) var prefersListLayout = true - @AppStorage(UserDefaultKey.shouldPromptCommunityModal.rawValue) var shouldPromptCommunityModal = true + @AppStorage(UserDefaultKey.openAIPrimerDisplayed.rawValue) var openAIPrimerDisplayed = false + @ObservedObject var viewModel: HomeFeedViewModel @State private var selection = Set() @@ -99,6 +101,17 @@ struct AnimatingCellHeight: AnimatableModifier { FilterSelectorView(viewModel: viewModel) } } + .sheet(isPresented: $showOpenAIVoices) { + OpenAIVoicesModal(audioController: audioController) + } + .onAppear { + showOpenAIVoices = true + + if !openAIPrimerDisplayed, !Voices.isOpenAIVoice(self.audioController.currentVoice) { + showOpenAIVoices = true + openAIPrimerDisplayed = true + } + } .toolbar { toolbarItems } diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift index 59715701e..9aca1e2fa 100644 --- a/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/Home/HomeFeedViewModel.swift @@ -33,7 +33,6 @@ import Views @Published var showLabelsSheet = false @Published var showFiltersModal = false - @Published var showCommunityModal = false @Published var featureItems = [LinkedItem]() @Published var listConfig: LibraryListConfig diff --git a/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift b/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift new file mode 100644 index 000000000..4388749ef --- /dev/null +++ b/apple/OmnivoreKit/Sources/App/Views/Home/OpenAIVoicesModal.swift @@ -0,0 +1,143 @@ +// swiftlint:disable line_length + +// +// CommunityModal.swift +// +// +// Created by Jackson Harper on 12/7/22. +// + +#if os(iOS) + import Foundation + import Models + import Services + import SwiftUI + import Views + + struct OpenAIVoiceItem { + let name: String + let key: String + } + + public struct OpenAIVoicesModal: View { + @Environment(\.dismiss) private var dismiss + + let audioController: AudioController + + let message: String = """ + We've added six new voices powered by OpenAI and enabled them for all users. If you are already using our Ultra Realistic voices, don't worry, trying these voices will not remove you from the ultra realistic beta. + + [Tell your friends about Omnivore](https://omnivore.app) + """ + + @State var playbackSample: String? + + let voices = [ + OpenAIVoiceItem(name: "Alloy", key: "openai-alloy"), + OpenAIVoiceItem(name: "Echo", key: "openai-echo"), + OpenAIVoiceItem(name: "Fable", key: "openai-fable"), + OpenAIVoiceItem(name: "Onyx", key: "openai-onyx"), + OpenAIVoiceItem(name: "Nova", key: "openai-nova"), + OpenAIVoiceItem(name: "Shimmer", key: "openai-shimmer") + ] + + var closeButton: some View { + Button(action: { + dismiss() + }, label: { + ZStack { + Circle() + .foregroundColor(Color.circleButtonBackground) + .frame(width: 30, height: 30) + + Image(systemName: "xmark") + .resizable(resizingMode: Image.ResizingMode.stretch) + .foregroundColor(Color.circleButtonForeground) + .aspectRatio(contentMode: .fit) + .font(Font.title.weight(.bold)) + .frame(width: 12, height: 12) + } + }) + } + + public var body: some View { + HStack { + Text("New voices powered by OpenAI") + .font(Font.system(size: 20, weight: .bold)) + Spacer() + closeButton + } + .padding(.top, 16) + .padding(.horizontal, 16) + + List { + Section { + let parsedMessage = try? AttributedString(markdown: message, + options: .init(interpretedSyntax: .inlineOnly)) + Text(parsedMessage ?? "") + .multilineTextAlignment(.leading) + .foregroundColor(Color.appGrayTextContrast) + .accentColor(.blue) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.top, 16) + } + + Section { + ForEach(voices, id: \.self.name) { voice in + voiceRow(for: voice) + } + } + } + .environmentObject(audioController) + } + + func voiceRow(for voice: OpenAIVoiceItem) -> some View { + Button(action: { + if audioController.isPlayingSample(voice: voice.key) { + playbackSample = nil + audioController.stopVoiceSample() + } + playbackSample = voice.key + audioController.currentVoice = voice.key + audioController.playVoiceSample(voice: voice.key) + Timer.scheduledTimer(withTimeInterval: 2.0, repeats: true) { timer in + let playing = audioController.isPlayingSample(voice: voice.key) + if playing { + playbackSample = voice.key + } else if !playing { + // If the playback sample is something else, its taken ownership + // of the value so we just ignore it and shut down our timer. + if playbackSample == voice.key { + playbackSample = nil + } + timer.invalidate() + } + } + }, label: { + HStack { + if playbackSample == voice.key { + Image(systemName: "stop.circle") + .font(.appTitleTwo) + .padding(.trailing, 16) + } else { + Image(systemName: "play.circle") + .font(.appTitleTwo) + .padding(.trailing, 16) + } + Text(voice.name) + Spacer() + + if audioController.currentVoice == voice.key { + if audioController.isPlaying, audioController.isLoading { + ProgressView() + } else { + Image(systemName: "checkmark") + } + } + }.contentShape(Rectangle()) + }) + .buttonStyle(PlainButtonStyle()) + .frame(maxWidth: .infinity) + } + } +#endif diff --git a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift index 2865d1887..a058c71b7 100644 --- a/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift +++ b/apple/OmnivoreKit/Sources/App/Views/RootView/RootViewModel.swift @@ -14,9 +14,6 @@ import Views public final class RootViewModel: ObservableObject { let services = Services() - @Published public var showNewFeaturePrimer = false - @AppStorage(UserDefaultKey.shouldShowNewFeaturePrimer.rawValue) var shouldShowNewFeaturePrimer = false - public init() { registerFonts() diff --git a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift index 1e732bf8c..8d6ee7c77 100644 --- a/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift +++ b/apple/OmnivoreKit/Sources/Utils/UserDefaultKeys.swift @@ -26,10 +26,9 @@ public enum UserDefaultKey: String { case recentSearchTerms case audioPlayerExpanded case themeName - case shouldShowNewFeaturePrimer + case openAIPrimerDisplayed case notificationsEnabled case deviceTokenID - case shouldPromptCommunityModal case userWordsPerMinute case hideFeatureSection case justifyText diff --git a/apple/OmnivoreKit/Sources/Views/CommunityModal.swift b/apple/OmnivoreKit/Sources/Views/CommunityModal.swift deleted file mode 100644 index 68e3d5e9f..000000000 --- a/apple/OmnivoreKit/Sources/Views/CommunityModal.swift +++ /dev/null @@ -1,129 +0,0 @@ -// -// CommunityModal.swift -// -// -// Created by Jackson Harper on 12/7/22. -// - -#if os(iOS) - import Foundation - import StoreKit - import SwiftUI - - // swiftlint:disable:next line_length - let tweetUrl = "https://twitter.com/intent/tweet?text=I%20recently%20started%20using%20@OmnivoreApp%20as%20a%20free,%20open-source%20read-it-later%20app.%20Check%20it%20out:%20https://omnivore.app" - - public struct CommunityModal: View { - @Environment(\.dismiss) private var dismiss - - let message: String = """ - Thank you for being a member of the Omnivore Community. - - Omnivore is a free and open-source project and relies on \ - help from our community to grow. Below are a few simple \ - things you can do to help us build a better Omnivore. - - If you would like to financially assist Omnivore \ - please [contribute on Open Collective](https://opencollective.com/omnivore). - """ - - public init() {} - -// var body: some View { -// ZStack { -// Image("Biz-card_2020") -// .resizable() -// .edgesIgnoringSafeArea(.all) -// closeButton -// } -// } - - var closeButton: some View { - VStack { - HStack { - Spacer() - Button { - dismiss() - } label: { - Image(systemName: "xmark.circle") - .padding(10) - } - } - .padding(.top, 5) - Spacer() - } - } - - public var header: some View { - VStack(spacing: 0) { - Text(LocalText.communityHeadline) - .font(.textToSpeechRead) - .foregroundColor(Color.appGrayTextContrast) - .frame(maxWidth: .infinity, alignment: .leading) - - HStack { - TextChip(text: "Help Wanted", color: Color.appBackground) - .frame(alignment: .leading) - TextChip(text: "Community", color: Color.green) - .frame(alignment: .leading) - } - .padding(.top, 10) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - - let links = [ - (title: LocalText.communityTweet, url: tweetUrl), - (title: LocalText.communityFollowTwitter, url: "https://twitter.com/omnivoreapp"), - (title: LocalText.communityJoinDiscord, url: "https://discord.gg/h2z5rppzz9"), - (title: LocalText.communityStarGithub, url: "https://github.com/omnivore-app/omnivore") - ] - - var buttonLinks: some View { - VStack(spacing: 15) { - Button(action: { - // swiftlint:disable:next line_length - if let scene = UIApplication.shared.connectedScenes.first(where: { $0.activationState == .foregroundActive }) as? UIWindowScene { - SKStoreReviewController.requestReview(in: scene) - } - }, label: { Text(LocalText.communityAppstoreReview) }) - .frame(maxWidth: .infinity, alignment: .leading) - - ForEach(links, id: \.url) { link in - if let url = URL(string: link.url) { - Link(link.title, destination: url) - .frame(maxWidth: .infinity, alignment: .leading) - } - } - }.frame(maxWidth: .infinity, alignment: .leading) - } - - public var body: some View { - VStack(spacing: 0) { - header - - let parsedMessage = try? AttributedString(markdown: message, - options: .init(interpretedSyntax: .inlineOnlyPreservingWhitespace)) - Text(parsedMessage ?? "") - .multilineTextAlignment(.leading) - .foregroundColor(Color.appGrayTextContrast) - .accentColor(.blue) - .frame(maxWidth: .infinity, alignment: .leading) - .padding(.top, 16) - - Spacer() - - buttonLinks - - Spacer() - - Button(action: { - dismiss() - }, label: { Text(LocalText.dismissButton) }) - .buttonStyle(PlainButtonStyle()) - .padding(.bottom, 16) - .frame(alignment: .bottom) - }.padding() - } - } -#endif