From a9b0af4fca1a1f2482de97e727c699a32ab44645 Mon Sep 17 00:00:00 2001 From: Jeremy Rand Date: Wed, 16 Mar 2022 20:37:18 -0400 Subject: [PATCH] Add an initial help screen that is visible on iPad and macOS. --- ListenerGS.xcodeproj/project.pbxproj | 32 ++++ ListenerGS/Info.plist | 2 +- ListenerGS/ListenerInfoView.swift | 60 +++++++ ListenerGS/MainView.swift | 2 +- ListenerGS/RichText/ColorSet.swift | 18 ++ ListenerGS/RichText/RichText.swift | 47 +++++ ListenerGS/RichText/RichTextEnums.swift | 30 ++++ ListenerGS/RichText/RichTextExtension.swift | 67 +++++++ ListenerGS/RichText/Webview.swift | 184 ++++++++++++++++++++ README.md | 6 +- 10 files changed, 444 insertions(+), 4 deletions(-) create mode 100644 ListenerGS/ListenerInfoView.swift create mode 100644 ListenerGS/RichText/ColorSet.swift create mode 100644 ListenerGS/RichText/RichText.swift create mode 100644 ListenerGS/RichText/RichTextEnums.swift create mode 100644 ListenerGS/RichText/RichTextExtension.swift create mode 100644 ListenerGS/RichText/Webview.swift diff --git a/ListenerGS.xcodeproj/project.pbxproj b/ListenerGS.xcodeproj/project.pbxproj index a1ad5d7..9b196ca 100644 --- a/ListenerGS.xcodeproj/project.pbxproj +++ b/ListenerGS.xcodeproj/project.pbxproj @@ -16,6 +16,12 @@ 9D2A6D2327E236E400DF3D85 /* ytcpsocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 9D51564E26A36B410075EBC7 /* ytcpsocket.c */; }; 9D2A6D2427E236FD00DF3D85 /* yudpsocket.c in Sources */ = {isa = PBXBuildFile; fileRef = 9D51564D26A36B410075EBC7 /* yudpsocket.c */; }; 9D2A6D2727E24BD600DF3D85 /* SpeechForwarderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */; }; + 9D2A6D2927E2A5E700DF3D85 /* ListenerInfoView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2827E2A5E700DF3D85 /* ListenerInfoView.swift */; }; + 9D2A6D3027E2AA6200DF3D85 /* RichTextExtension.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2B27E2AA6200DF3D85 /* RichTextExtension.swift */; }; + 9D2A6D3127E2AA6200DF3D85 /* RichTextEnums.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2C27E2AA6200DF3D85 /* RichTextEnums.swift */; }; + 9D2A6D3227E2AA6200DF3D85 /* Webview.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2D27E2AA6200DF3D85 /* Webview.swift */; }; + 9D2A6D3327E2AA6200DF3D85 /* ColorSet.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2E27E2AA6200DF3D85 /* ColorSet.swift */; }; + 9D2A6D3427E2AA6200DF3D85 /* RichText.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D2A6D2F27E2AA6200DF3D85 /* RichText.swift */; }; 9D5155F326A1EF7B0075EBC7 /* ListenerGSApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */; }; 9D5155F726A1EF7C0075EBC7 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */; }; 9D5155FA26A1EF7C0075EBC7 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 9D5155F926A1EF7C0075EBC7 /* Preview Assets.xcassets */; }; @@ -61,6 +67,12 @@ 9D0DC15826F2E47A007EB92D /* ListenerGS.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = ListenerGS.entitlements; sourceTree = ""; }; 9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSServerMock.swift; sourceTree = ""; }; 9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechForwarderMock.swift; sourceTree = ""; }; + 9D2A6D2827E2A5E700DF3D85 /* ListenerInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListenerInfoView.swift; sourceTree = ""; }; + 9D2A6D2B27E2AA6200DF3D85 /* RichTextExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichTextExtension.swift; sourceTree = ""; }; + 9D2A6D2C27E2AA6200DF3D85 /* RichTextEnums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichTextEnums.swift; sourceTree = ""; }; + 9D2A6D2D27E2AA6200DF3D85 /* Webview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Webview.swift; sourceTree = ""; }; + 9D2A6D2E27E2AA6200DF3D85 /* ColorSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorSet.swift; sourceTree = ""; }; + 9D2A6D2F27E2AA6200DF3D85 /* RichText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichText.swift; sourceTree = ""; }; 9D5155EF26A1EF7B0075EBC7 /* ListenerGS.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ListenerGS.app; sourceTree = BUILT_PRODUCTS_DIR; }; 9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListenerGSApp.swift; sourceTree = ""; }; 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; @@ -120,6 +132,18 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 9D2A6D2A27E2AA6200DF3D85 /* RichText */ = { + isa = PBXGroup; + children = ( + 9D2A6D2B27E2AA6200DF3D85 /* RichTextExtension.swift */, + 9D2A6D2C27E2AA6200DF3D85 /* RichTextEnums.swift */, + 9D2A6D2D27E2AA6200DF3D85 /* Webview.swift */, + 9D2A6D2E27E2AA6200DF3D85 /* ColorSet.swift */, + 9D2A6D2F27E2AA6200DF3D85 /* RichText.swift */, + ); + path = RichText; + sourceTree = ""; + }; 9D5155E626A1EF7B0075EBC7 = { isa = PBXGroup; children = ( @@ -149,12 +173,14 @@ 9D0DC15826F2E47A007EB92D /* ListenerGS.entitlements */, 9D5155F226A1EF7B0075EBC7 /* ListenerGSApp.swift */, 9D6F27082728EF410089585E /* MainView.swift */, + 9D2A6D2827E2A5E700DF3D85 /* ListenerInfoView.swift */, 9DD67CEF2728F5B700243FC6 /* DestinationsView.swift */, 9DCCDACB271FB87100F311DF /* GSDestinations.swift */, 9DD8905F2772D3B20084A894 /* GSView.swift */, 9D6ED239271E6BD600D773CD /* SpeechForwarder.swift */, 9D05BAA927DFDE6300D9CC4B /* GSConnection.swift */, 9DD8905E27726C140084A894 /* ListenerGS Icon.pxm */, + 9D2A6D2A27E2AA6200DF3D85 /* RichText */, 9D51566326A36F530075EBC7 /* BinUtils */, 9D51563626A36AD60075EBC7 /* SwiftSocket */, 9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */, @@ -379,17 +405,23 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 9D2A6D3127E2AA6200DF3D85 /* RichTextEnums.swift in Sources */, 9D51565726A36B410075EBC7 /* TCPClient.swift in Sources */, 9D05BAAA27DFDE6300D9CC4B /* GSConnection.swift in Sources */, + 9D2A6D3227E2AA6200DF3D85 /* Webview.swift in Sources */, 9D6F27092728EF410089585E /* MainView.swift in Sources */, 9D51565526A36B410075EBC7 /* UDPClient.swift in Sources */, 9DD67CF02728F5B700243FC6 /* DestinationsView.swift in Sources */, 9D51565626A36B410075EBC7 /* Socket.swift in Sources */, + 9D2A6D3427E2AA6200DF3D85 /* RichText.swift in Sources */, + 9D2A6D3327E2AA6200DF3D85 /* ColorSet.swift in Sources */, 9D51565326A36B410075EBC7 /* yudpsocket.c in Sources */, 9DD890602772D3B20084A894 /* GSView.swift in Sources */, 9D51565226A36B410075EBC7 /* Result.swift in Sources */, 9D6ED23A271E6BD600D773CD /* SpeechForwarder.swift in Sources */, + 9D2A6D3027E2AA6200DF3D85 /* RichTextExtension.swift in Sources */, 9D5155F326A1EF7B0075EBC7 /* ListenerGSApp.swift in Sources */, + 9D2A6D2927E2A5E700DF3D85 /* ListenerInfoView.swift in Sources */, 9D51566526A36F6D0075EBC7 /* BinUtils.swift in Sources */, 9DCCDACC271FB87100F311DF /* GSDestinations.swift in Sources */, 9D51565426A36B410075EBC7 /* ytcpsocket.c in Sources */, diff --git a/ListenerGS/Info.plist b/ListenerGS/Info.plist index d58fa1e..a7d1a70 100644 --- a/ListenerGS/Info.plist +++ b/ListenerGS/Info.plist @@ -19,7 +19,7 @@ CFBundleShortVersionString 1.0 CFBundleVersion - 548 + 598 LSApplicationCategoryType public.app-category.utilities LSRequiresIPhoneOS diff --git a/ListenerGS/ListenerInfoView.swift b/ListenerGS/ListenerInfoView.swift new file mode 100644 index 0000000..aa9ab1b --- /dev/null +++ b/ListenerGS/ListenerInfoView.swift @@ -0,0 +1,60 @@ +// +// ListenerInfoView.swift +// ListenerGS +// +// Created by Jeremy Rand on 2022-03-16. +// + +import SwiftUI + +struct ListenerInfoView: View { + var body: some View { + ScrollView { + RichText(html: +""" + + + +

+ListenerGS allows you to use your modern device as a speech recognition peripheral for a network capable +Apple IIGS. For more information about how to use the app and links to the software you need to download +to your GS, please visit https://www.rand-emonium.com/listenergs/. +

+ +

+Once you have the software installed and configured on your Apple IIGS, you should launch an desktop application +that accepts text. The Teach application is an example of an application that would work. Make sure there is a +window open which you can type into and then open the Listener NDA from under the Apple menu. The Listener NDA window +will say it is waiting for a connection. +

+ +

+In this app, tap the "+" button and enter the IP address or hostname of your Apple IIGS. You can enter multiple IP +addresses and hostnames if you have multiple machines. Your destinations are synced through iCloud so if you have +multiple modern devices, you should find the IP addresses are mirrored to those other devices. +

+ +

Select one of these destinations and tap the "Connect" button to bring up a network connection to your Apple IIGS. +On the GS, you should find the NDA window also indicates that the connection is up. Then tap the "Listen and Send Text" +button. Speak clearly and you should find that your words are typed into the window on your GS. If the NDA window was +top-most when you started speaking, you should find that it goes to the back.

+ +

+Top "Stop Listening" when you want to stop entering text through speech and "Disconnect" when you are done using the app. +

+ + + +""") + // .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .frame(maxWidth: .infinity) + .navigationBarTitle("Welcome to ListenerGS!") + } +} + +struct ListenerInfoView_Previews: PreviewProvider { + static var previews: some View { + ListenerInfoView() + } +} diff --git a/ListenerGS/MainView.swift b/ListenerGS/MainView.swift index 0c11547..4ae9608 100644 --- a/ListenerGS/MainView.swift +++ b/ListenerGS/MainView.swift @@ -11,7 +11,7 @@ struct MainView: View { var body: some View { NavigationView { DestinationsView() - EmptyView() // JSR_TODO - Maybe display some instructions here. + ListenerInfoView() } } } diff --git a/ListenerGS/RichText/ColorSet.swift b/ListenerGS/RichText/ColorSet.swift new file mode 100644 index 0000000..bb4ae12 --- /dev/null +++ b/ListenerGS/RichText/ColorSet.swift @@ -0,0 +1,18 @@ +// +// ColorSet.swift +// +// +// Created by 이웅재(NuPlay) on 2022/01/04. +// https://github.com/NuPlay/RichText + +import SwiftUI + +public struct ColorSet { + var light: String + var dark: String + + public init(light: String, dark: String) { + self.light = light + self.dark = dark + } +} diff --git a/ListenerGS/RichText/RichText.swift b/ListenerGS/RichText/RichText.swift new file mode 100644 index 0000000..8ae4ee4 --- /dev/null +++ b/ListenerGS/RichText/RichText.swift @@ -0,0 +1,47 @@ +// +// RichText.swift +// +// +// Created by 이웅재(NuPlay) on 2021/07/26. +// https://github.com/NuPlay/RichText + +import SwiftUI + +public struct RichText: View { + @State private var dynamicHeight: CGFloat = .zero + + let html: String + + var lineHeight: CGFloat = 170 + var imageRadius: CGFloat = 0 + var fontType: fontType = .system + + var colorScheme: colorScheme = .automatic + var colorImportant: Bool = false + + var placeholder: AnyView? + + var linkOpenType: linkOpenType = .SFSafariView + var linkColor: ColorSet = ColorSet(light: "#007AFF", dark: "#0A84FF") + + public init(html: String) { + self.html = html + } + + public var body: some View { + ZStack(alignment: .top) { + Webview(dynamicHeight: $dynamicHeight, html: html, lineHeight: lineHeight, imageRadius: imageRadius, fontType: fontType, colorScheme: colorScheme, colorImportant: colorImportant, linkOpenType: linkOpenType, linkColor: linkColor) + .frame(height: dynamicHeight) + + if self.dynamicHeight == 0 { + placeholder + } + } + } +} + +struct RichText_Previews: PreviewProvider { + static var previews: some View { + RichText(html: "") + } +} diff --git a/ListenerGS/RichText/RichTextEnums.swift b/ListenerGS/RichText/RichTextEnums.swift new file mode 100644 index 0000000..6145546 --- /dev/null +++ b/ListenerGS/RichText/RichTextEnums.swift @@ -0,0 +1,30 @@ +// +// RichTextEnums.swift +// +// +// Created by 이웅재(NuPlay) on 2021/07/26. +// https://github.com/NuPlay/RichText + +import Foundation + +public enum colorScheme: String { + case light = "light" + case dark = "dark" + case automatic = "automatic" +} + +public enum fontType: String { + case system = "system" + case monospaced = "monospaced" + case italic = "italic" + + @available(*, deprecated, renamed: "system") + case `default` = "default" +} + +public enum linkOpenType: String { + case SFSafariView = "SFSafariView" + case SFSafariViewWithReader = "SFSafariViewWithReader" + case Safari = "Safari" + case none = "none" +} diff --git a/ListenerGS/RichText/RichTextExtension.swift b/ListenerGS/RichText/RichTextExtension.swift new file mode 100644 index 0000000..0d13c0b --- /dev/null +++ b/ListenerGS/RichText/RichTextExtension.swift @@ -0,0 +1,67 @@ +// +// RichTextExtension.swift +// +// +// Created by 이웅재(NuPlay) on 2021/08/27. +// https://github.com/NuPlay/RichText + +import SwiftUI + +extension RichText { + + public func lineHeight(_ lineHeight: CGFloat) -> RichText { + var result = self + + result.lineHeight = lineHeight + return result + } + + public func imageRadius(_ imageRadius: CGFloat) -> RichText { + var result = self + + result.imageRadius = imageRadius + return result + } + + public func fontType(_ fontType: fontType) -> RichText { + var result = self + + result.fontType = fontType + return result + } + + public func colorScheme(_ colorScheme: colorScheme) -> RichText { + var result = self + + result.colorScheme = colorScheme + return result + } + + public func colorImportant(_ colorImportant: Bool) -> RichText { + var result = self + + result.colorImportant = colorImportant + return result + } + + public func placeholder(@ViewBuilder content: () -> T) -> RichText where T: View { + var result = self + + result.placeholder = AnyView(content()) + return result + } + + public func linkOpenType(_ linkOpenType: linkOpenType) -> RichText { + var result = self + + result.linkOpenType = linkOpenType + return result + } + + public func linkColor(_ linkColor: ColorSet) -> RichText { + var result = self + + result.linkColor = linkColor + return result + } +} diff --git a/ListenerGS/RichText/Webview.swift b/ListenerGS/RichText/Webview.swift new file mode 100644 index 0000000..9f87fc1 --- /dev/null +++ b/ListenerGS/RichText/Webview.swift @@ -0,0 +1,184 @@ +// +// Webview.swift +// +// +// Created by 이웅재(NuPlay) on 2021/07/26. +// https://github.com/NuPlay/RichText + +import SwiftUI +import WebKit +import SafariServices + +struct Webview: UIViewRepresentable { + + @Binding var dynamicHeight: CGFloat + private var webview: WKWebView = WKWebView() + + let html: String + + let lineHeight: CGFloat + let imageRadius: CGFloat + let fontType: fontType + + let colorScheme: colorScheme + let colorImportant: Bool + + let linkOpenType: linkOpenType + let linkColor: ColorSet + + public init(dynamicHeight: Binding, webview: WKWebView = WKWebView(), html: String, lineHeight: CGFloat, imageRadius: CGFloat, fontType: fontType, colorScheme: colorScheme, colorImportant: Bool, linkOpenType: linkOpenType, linkColor: ColorSet) { + self._dynamicHeight = dynamicHeight + self.webview = webview + + self.html = html + + self.lineHeight = lineHeight + self.imageRadius = imageRadius + self.fontType = fontType + + self.colorScheme = colorScheme + self.colorImportant = colorImportant + + self.linkOpenType = linkOpenType + self.linkColor = linkColor + } + + public class Coordinator: NSObject, WKNavigationDelegate { + var parent: Webview + + init(_ parent: Webview) { + self.parent = parent + } + + public func webView(_ webView: WKWebView, didFinish navigation: WKNavigation!) { + webView.evaluateJavaScript("document.documentElement.scrollHeight", completionHandler: { (height, _) in + DispatchQueue.main.async { + self.parent.dynamicHeight = height as! CGFloat + } + }) + } + + func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) { + if navigationAction.navigationType == WKNavigationType.linkActivated { + if let url = navigationAction.request.url { + + let root = UIApplication.shared.windows.first?.rootViewController + switch self.parent.linkOpenType { + case .SFSafariView: + root?.present(SFSafariViewController(url: url), animated: true, completion: nil) + case .SFSafariViewWithReader: + let configuration = SFSafariViewController.Configuration() + configuration.entersReaderIfAvailable = true + root?.present(SFSafariViewController(url: url, configuration: configuration), animated: true, completion: nil) + case .Safari : + UIApplication.shared.open(url) + case .none : + print(url) + } + } + + decisionHandler(WKNavigationActionPolicy.cancel) + return + } + print("no link") + decisionHandler(WKNavigationActionPolicy.allow) + } + } + + public func makeCoordinator() -> Coordinator { + Coordinator(self) + } + + public func makeUIView(context: Context) -> WKWebView { + webview.scrollView.bounces = false + webview.navigationDelegate = context.coordinator + webview.scrollView.isScrollEnabled = false + let htmlStart = """ + + + + + """ + let htmlEnd = "" + let htmlString = "\(htmlStart)\(css(colorScheme: self.colorScheme))\(html)\(htmlEnd)" + webview.loadHTMLString(htmlString, baseURL: nil) + webview.isOpaque = false + webview.backgroundColor = UIColor.clear + webview.scrollView.backgroundColor = UIColor.clear + // + return webview + } + + public func updateUIView(_ uiView: WKWebView, context: Context) { + let htmlStart = """ + + + + + """ + let htmlEnd = "" + let htmlString = "\(htmlStart)\(css(colorScheme: self.colorScheme))\(html)\(htmlEnd)" + uiView.loadHTMLString(htmlString, baseURL: nil) + } + + func css(colorScheme: colorScheme) -> String { + switch colorScheme { + case .light: + return """ + + + """ + case .dark : + return """ + + + """ + case .automatic: + return """ + + + """ + } + } + + func fontName(fontType: fontType) -> String { + switch fontType { + case .system: + return "-apple-system" + case .monospaced: + return UIFont.monospacedSystemFont(ofSize: 17, weight: .regular).fontName + case .italic: + return UIFont.italicSystemFont(ofSize: 17).fontName + + default : + return "-apple-system" + } + } +} diff --git a/README.md b/README.md index 1793247..4a7c47a 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,10 @@ I am getting close to making this available as a release that anyone can obtain. It relies on the [Speech Framework](https://developer.apple.com/documentation/speech) built into recent versions of iOS. -This app uses [SwiftSocket v2.1.0](https://github.com/swiftsocket/SwiftSocket/tree/2.1.0) for opening the TCP connection to the Apple IIgs. -It also uses [BinUtils](https://github.com/nst/BinUtils) for packing/unpacking structures on the TCP connection. +This app uses: +* [SwiftSocket v2.1.0](https://github.com/swiftsocket/SwiftSocket/tree/2.1.0) for opening the TCP connection to the Apple IIgs. +* [BinUtils](https://github.com/nst/BinUtils) for packing/unpacking structures on the TCP connection. +* [RichText v1.7.0](https://github.com/NuPlay/RichText/releases/tag/1.7.0) for displaying a nice startup screen on iPad and macOS. ## Warning