From 841d262c96b8793c2447618c8d081b0caf69ac1a Mon Sep 17 00:00:00 2001 From: Viktor Kushnerov Date: Mon, 9 Mar 2020 23:01:05 +0300 Subject: [PATCH] Cambridge dictionary #77: Speak words. --- README.md | 5 + ReaderTranslator.xcodeproj/project.pbxproj | 8 ++ .../Components/SpeechSynthesizer.swift | 10 +- ReaderTranslator/Stores/CambridgeStore.swift | 114 ++++++++++++++++++ ReaderTranslator/Stores/LongmanStore.swift | 7 +- ReaderTranslator/Stores/Store.swift | 4 +- ReaderTranslatorMac/Info.plist | 2 +- 7 files changed, 141 insertions(+), 9 deletions(-) create mode 100644 ReaderTranslator/Stores/CambridgeStore.swift diff --git a/README.md b/README.md index 0b658e4..dd3dffd 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,11 @@ Explore features, limitations and bugs *SwiftUI, Combine and Catalyst*. ## Releases ### Download .dmg from [here](https://github.com/filimo/ReaderTranslator/releases) +**1.12.2** +Speaking selected words and phrases by Cambridge speakers + +- [Cambridge dictionary #77](https://github.com/filimo/ReaderTranslator/issues/77) + **1.12.0** - [DeepL implemented #79](https://github.com/filimo/ReaderTranslator/issues/79) diff --git a/ReaderTranslator.xcodeproj/project.pbxproj b/ReaderTranslator.xcodeproj/project.pbxproj index bc993fa..a8c689c 100644 --- a/ReaderTranslator.xcodeproj/project.pbxproj +++ b/ReaderTranslator.xcodeproj/project.pbxproj @@ -125,6 +125,9 @@ F058C7FE2397FBCE002C84F0 /* longman.json in Resources */ = {isa = PBXBuildFile; fileRef = F058C7F12397A180002C84F0 /* longman.json */; }; F058C800239930F2002C84F0 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F058C7FF23992F92002C84F0 /* BookmarksView.swift */; }; F058C801239930F3002C84F0 /* BookmarksView.swift in Sources */ = {isa = PBXBuildFile; fileRef = F058C7FF23992F92002C84F0 /* BookmarksView.swift */; }; + F0623E422416D1C2005BC86C /* CambridgeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0623E412416D1C2005BC86C /* CambridgeStore.swift */; }; + F0623E432416D1CA005BC86C /* CambridgeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0623E412416D1C2005BC86C /* CambridgeStore.swift */; }; + F0623E442416D1D6005BC86C /* CambridgeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = F0623E412416D1C2005BC86C /* CambridgeStore.swift */; }; F064B4A423CC569400F28314 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F064B4A323CC569400F28314 /* CGFloat.swift */; }; F064B4A523CC569400F28314 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F064B4A323CC569400F28314 /* CGFloat.swift */; }; F064B4A623CC569400F28314 /* CGFloat.swift in Sources */ = {isa = PBXBuildFile; fileRef = F064B4A323CC569400F28314 /* CGFloat.swift */; }; @@ -645,6 +648,7 @@ F058C7F12397A180002C84F0 /* longman.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = longman.json; sourceTree = ""; }; F058C7FF23992F92002C84F0 /* BookmarksView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BookmarksView.swift; sourceTree = ""; }; F062029423812264002EEAEE /* YTranslatorRepresenter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = YTranslatorRepresenter.swift; sourceTree = ""; }; + F0623E412416D1C2005BC86C /* CambridgeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CambridgeStore.swift; sourceTree = ""; }; F064B4A323CC569400F28314 /* CGFloat.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGFloat.swift; sourceTree = ""; }; F064B4A923CC598400F28314 /* NumberFormatter.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NumberFormatter.swift; sourceTree = ""; }; F065095623ADF6A3003D2410 /* AudioStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AudioStore.swift; sourceTree = ""; }; @@ -1253,6 +1257,7 @@ F065095D23AE0353003D2410 /* ViewsStore.swift */, F0418B97239A32FD00B59C66 /* BookmarksStore.swift */, F0553727239BE3C700BA24BA /* LongmanStore.swift */, + F0623E412416D1C2005BC86C /* CambridgeStore.swift */, F0039EC223447E24002F3F95 /* SharedContainer.swift */, F06DB115234611BB00C2DE90 /* ExtensionManager.swift */, ); @@ -1964,6 +1969,7 @@ F0C4EDA7234926D400CCD97A /* ReversoRepresenter.swift in Sources */, F0EE0A1023478C86004A5EAD /* DOMEvent.swift in Sources */, F0C4EDAA2349271C00CCD97A /* ViewRepresentable.swift in Sources */, + F0623E432416D1CA005BC86C /* CambridgeStore.swift in Sources */, F0AD8B1F236414070017C22F /* TranslateAction.swift in Sources */, F06DB1002344856300C2DE90 /* Store.swift in Sources */, F0BB43742344844800ADBEF1 /* PDFKitView.swift in Sources */, @@ -2235,6 +2241,7 @@ F099421A23AD43D8003CF1EB /* YTranslatorView.swift in Sources */, F099422D23AD44E7003CF1EB /* AudioPlayer.swift in Sources */, F0C02D4823B27CA100B393A5 /* Logger.swift in Sources */, + F0623E442416D1D6005BC86C /* CambridgeStore.swift in Sources */, F099423123AD450A003CF1EB /* WKPageView.swift in Sources */, F099422F23AD44F9003CF1EB /* OpenPanel.swift in Sources */, F099422423AD445F003CF1EB /* WebViewContainer.swift in Sources */, @@ -2267,6 +2274,7 @@ F0F256BD233D309F00C9D719 /* String.swift in Sources */, F0AB12A3233F5798005B9F2A /* ReaderView_Pdf.swift in Sources */, F08D9404239C0E4400147ECE /* BookmarksView_List_Row.swift in Sources */, + F0623E422416D1C2005BC86C /* CambridgeStore.swift in Sources */, F0AD8B1E236414070017C22F /* TranslateAction.swift in Sources */, F0EDFB25239E49480048CFD1 /* AudioPlayer.swift in Sources */, F02B04B723A2894700F93B84 /* PeerBrowser.swift in Sources */, diff --git a/ReaderTranslator/Components/SpeechSynthesizer.swift b/ReaderTranslator/Components/SpeechSynthesizer.swift index f1c30f0..cbd7b29 100644 --- a/ReaderTranslator/Components/SpeechSynthesizer.swift +++ b/ReaderTranslator/Components/SpeechSynthesizer.swift @@ -74,9 +74,13 @@ class SpeechSynthesizer { if stopSpeaking { return } } - cancellableLongmanSpeak = LongmanStore.shared.fetchInfo(text: text) - .sink { isSoundExist in - if isSoundExist { + cancellableLongmanSpeak = Publishers.CombineLatest( + LongmanStore.shared.fetchInfo(text: text), + CambridgeStore.shared.fetchInfo(text: text)) + .sink { isLongmanSoundExist, isCambridgeSoundExist in + if isCambridgeSoundExist { + CambridgeStore.shared.play() + }else if isLongmanSoundExist { LongmanStore.shared.play() } else { speakByEngine(text: text, voiceName: voiceName, isVoiceEnabled: isVoiceEnabled) diff --git a/ReaderTranslator/Stores/CambridgeStore.swift b/ReaderTranslator/Stores/CambridgeStore.swift new file mode 100644 index 0000000..cfd81a4 --- /dev/null +++ b/ReaderTranslator/Stores/CambridgeStore.swift @@ -0,0 +1,114 @@ +// +// CambridgeStore.swift +// ReaderTranslator +// +// Created by Viktor Kushnerov on 9/3/20. +// Copyright © 2020 Viktor Kushnerov. All rights reserved. +// + +import AVFoundation +import Combine +import Foundation +import SwiftSoup +import SwiftUI + +private var cancellable: AnyCancellable? +private var player: AVAudioNetPlayer? + +struct CambridgeSentence: Hashable { + static let empty = Self(text: "No sentences", url: URL.empty) + + let text: String + let url: URL +} + +typealias CambridgeSentences = [CambridgeSentence] + +final class CambridgeStore: NSObject, ObservableObject { + private override init() { super.init() } + static var shared = CambridgeStore() + + @Published var audioRate: Float = 1 + @Published var word = "" + + private let defaultURL = "https://dictionary.cambridge.org/dictionary/english-russian/" + private var audioUrls = Stack() + + func fetchInfo(text: String) -> AnyPublisher { + let text = text.encodeUrl + guard let url = URL(string: "\(defaultURL)\(text)") else { + return Just(false).eraseToAnyPublisher() + } + + return URLSession.shared.dataTaskPublisher(for: url) + .map { + guard let html = String(data: $0.data, encoding: .utf8) else { return false } + do { + let document = try SwiftSoup.parse(html) + + self.audioUrls.removeAll() + let isBreExist = self.addAudio(selector: ".uk [type='audio/mpeg']", document: document) + let isAmeExist = self.addAudio(selector: ".us [type='audio/mpeg']", document: document) + + return isBreExist || isAmeExist + } catch { + Logger.log(type: .error, value: error) + } + + return false + } + .catch { _ in + Just(false) + } + .eraseToAnyPublisher() + } +} + +extension CambridgeStore { + func addAudio(url: URL) { + audioUrls.push(url) + } + + private func addAudio(selector: String, document: Document) -> Bool { + do { + guard let elm = try document.select(selector).first else { return false } + let string = try elm.attr("src") + guard let url = URL(string: "https://dictionary.cambridge.org/\(string)") else { return false } + + addAudio(url: url) + return true + } catch { + Logger.log(type: .error, value: error) + } + return false + } +} + +extension CambridgeStore { + func play() { + guard let url = audioUrls.pop() else { return } + + if AudioStore.shared.isSpeakWords { + player = AVAudioNetPlayer() + player?.delegate = self + player?.play(url: url) + } + } +} + +extension CambridgeStore: AVAudioNetPlayerDelegate { + func audioPlayerLoadDidFinishDidOccur() {} + + func audioPlayerCreateSuccessOccur(player: AVAudioPlayer) { + player.enableRate = true + player.rate = audioRate + player.volume = AudioStore.shared.wordsVolume + player.play() + } + + func audioPlayerLoadErrorDidOccur() { play() } + func audioPlayerCreateErrorDidOccur() { play() } + + func audioPlayerDidFinishPlaying(_: AVAudioPlayer, successfully _: Bool) { play() } + func audioPlayerDecodeErrorDidOccur(_: AVAudioPlayer, error _: Error?) { play() } +} diff --git a/ReaderTranslator/Stores/LongmanStore.swift b/ReaderTranslator/Stores/LongmanStore.swift index a45c247..aff867e 100644 --- a/ReaderTranslator/Stores/LongmanStore.swift +++ b/ReaderTranslator/Stores/LongmanStore.swift @@ -38,7 +38,6 @@ final class LongmanStore: NSObject, ObservableObject { } private let defaultURL = "https://www.ldoceonline.com/dictionary/" - private var audioUrls = Stack() func fetchInfo(text: String) -> AnyPublisher { let text = text.replacingOccurrences(of: " ", with: "-") @@ -52,7 +51,7 @@ final class LongmanStore: NSObject, ObservableObject { do { let document = try SwiftSoup.parse(html) - self.audioUrls.removeAll() + Store.shared.audioUrls.removeAll() let isBreExist = self.addAudio(selector: ".brefile", document: document) let isAmeExist = self.addAudio(selector: ".amefile", document: document) @@ -101,7 +100,7 @@ extension LongmanStore { } func addAudio(url: URL) { - audioUrls.push(url) + Store.shared.audioUrls.push(url) } private func addAudio(selector: String, document: Document) -> Bool { @@ -121,7 +120,7 @@ extension LongmanStore { extension LongmanStore { func play() { - guard let url = audioUrls.pop() else { return } + guard let url = Store.shared.audioUrls.pop() else { return } if AudioStore.shared.isSpeakWords { player = AVAudioNetPlayer() diff --git a/ReaderTranslator/Stores/Store.swift b/ReaderTranslator/Stores/Store.swift index 2a17433..9c29bf1 100644 --- a/ReaderTranslator/Stores/Store.swift +++ b/ReaderTranslator/Stores/Store.swift @@ -12,7 +12,9 @@ import SwiftUI final class Store: ObservableObject { private init() {} static var shared = Store() - + + var audioUrls = Stack() + let maxViewWidth: CGFloat = 400 @Published var translateAction = TranslateAction() diff --git a/ReaderTranslatorMac/Info.plist b/ReaderTranslatorMac/Info.plist index 0967d9c..9a23862 100644 --- a/ReaderTranslatorMac/Info.plist +++ b/ReaderTranslatorMac/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType $(PRODUCT_BUNDLE_PACKAGE_TYPE) CFBundleShortVersionString - 1.12.1 + 1.12.2 CFBundleVersion 1800 LSApplicationCategoryType