Add an initial help screen that is visible on iPad and macOS.

This commit is contained in:
Jeremy Rand 2022-03-16 20:37:18 -04:00
parent d7c430f588
commit a9b0af4fca
10 changed files with 444 additions and 4 deletions

View File

@ -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 = "<group>"; };
9D2A6D1D27E235E400DF3D85 /* GSServerMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GSServerMock.swift; sourceTree = "<group>"; };
9D2A6D2627E24BD600DF3D85 /* SpeechForwarderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SpeechForwarderMock.swift; sourceTree = "<group>"; };
9D2A6D2827E2A5E700DF3D85 /* ListenerInfoView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ListenerInfoView.swift; sourceTree = "<group>"; };
9D2A6D2B27E2AA6200DF3D85 /* RichTextExtension.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichTextExtension.swift; sourceTree = "<group>"; };
9D2A6D2C27E2AA6200DF3D85 /* RichTextEnums.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichTextEnums.swift; sourceTree = "<group>"; };
9D2A6D2D27E2AA6200DF3D85 /* Webview.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = Webview.swift; sourceTree = "<group>"; };
9D2A6D2E27E2AA6200DF3D85 /* ColorSet.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = ColorSet.swift; sourceTree = "<group>"; };
9D2A6D2F27E2AA6200DF3D85 /* RichText.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = RichText.swift; sourceTree = "<group>"; };
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 = "<group>"; };
9D5155F626A1EF7C0075EBC7 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
@ -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 = "<group>";
};
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 */,

View File

@ -19,7 +19,7 @@
<key>CFBundleShortVersionString</key>
<string>1.0</string>
<key>CFBundleVersion</key>
<string>548</string>
<string>598</string>
<key>LSApplicationCategoryType</key>
<string>public.app-category.utilities</string>
<key>LSRequiresIPhoneOS</key>

View File

@ -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:
"""
<html>
<body>
<p>
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 <a href="https://www.rand-emonium.com/listenergs/">https://www.rand-emonium.com/listenergs/</a>.
</p>
<p>
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.
</p>
<p>
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.
</p>
<p>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.</p>
<p>
Top "Stop Listening" when you want to stop entering text through speech and "Disconnect" when you are done using the app.
</p>
</body>
</html>
""")
// .frame(maxWidth: .infinity, maxHeight: .infinity)
}
.frame(maxWidth: .infinity)
.navigationBarTitle("Welcome to ListenerGS!")
}
}
struct ListenerInfoView_Previews: PreviewProvider {
static var previews: some View {
ListenerInfoView()
}
}

View File

@ -11,7 +11,7 @@ struct MainView: View {
var body: some View {
NavigationView {
DestinationsView()
EmptyView() // JSR_TODO - Maybe display some instructions here.
ListenerInfoView()
}
}
}

View File

@ -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
}
}

View File

@ -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: "")
}
}

View File

@ -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"
}

View File

@ -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<T>(@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
}
}

View File

@ -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<CGFloat>, 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 = """
<HTML>
<head>
<meta name='viewport' content='width=device-width, shrink-to-fit=YES, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no'>
</head>
"""
let htmlEnd = "</BODY></HTML>"
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 = """
<HTML>
<head>
<meta name='viewport' content='width=device-width, shrink-to-fit=YES, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0, user-scalable=no'>
</head>
"""
let htmlEnd = "</BODY></HTML>"
let htmlString = "\(htmlStart)\(css(colorScheme: self.colorScheme))\(html)\(htmlEnd)"
uiView.loadHTMLString(htmlString, baseURL: nil)
}
func css(colorScheme: colorScheme) -> String {
switch colorScheme {
case .light:
return """
<style type='text/css'>
img{max-height: 100%; min-height: 100%; height:auto; max-width: 100%; width:auto;margin-bottom:5px; border-radius: \(imageRadius)px;}
h1, h2, h3, h4, h5, h6, p, dl, ol, ul, pre, blockquote {text-align:left|right|center; line-height: \(lineHeight)%; font-family: '\(fontName(fontType: self.fontType))'; color: #000000 \(colorImportant == false ? "" : "!important"); }
iframe{width:100%; height:250px;}
a:link {color: \(linkColor.light);}
A {text-decoration: none;}
</style>
<BODY>
"""
case .dark :
return """
<style type='text/css'>
img{max-height: 100%; min-height: 100%; height:auto; max-width: 100%; width:auto;margin-bottom:5px; border-radius: \(imageRadius)px;}
h1, h2, h3, h4, h5, h6, p, dl, ol, ul, pre, blockquote {text-align:left|right|center; line-height: \(lineHeight)%; font-family: '\(fontName(fontType: self.fontType))'; color: #F2F2F2 \(colorImportant == false ? "" : "!important"); }
iframe{width:100%; height:250px;}
a:link {color: \(linkColor.dark);}
A {text-decoration: none;}
</style>
<BODY>
"""
case .automatic:
return """
<style type='text/css'>
@media (prefers-color-scheme: light) {
img{max-height: 100%; min-height: 100%; height:auto; max-width: 100%; width:auto;margin-bottom:5px; border-radius: \(imageRadius)px;}
h1, h2, h3, h4, h5, h6, p, dl, ol, ul, pre, blockquote {text-align:left|right|center; line-height: \(lineHeight)%; font-family: '\(fontName(fontType: self.fontType))'; color: #000000 \(colorImportant == false ? "" : "!important"); }
iframe{width:100%; height:250px;}
a:link {color: \(linkColor.light);}
A {text-decoration: none;}
}
@media (prefers-color-scheme: dark) {
img{max-height: 100%; min-height: 100%; height:auto; max-width: 100%; width:auto;margin-bottom:5px; border-radius: \(imageRadius)px;}
h1, h2, h3, h4, h5, h6, p, dl, ol, ul, pre, blockquote {text-align:left|right|center; line-height: \(lineHeight)%; font-family: '\(fontName(fontType: self.fontType))'; color: #F2F2F2 \(colorImportant == false ? "" : "!important"); }
iframe{width:100%; height:250px;}
a:link {color: \(linkColor.dark);}
A {text-decoration: none;}
}
</style>
<BODY>
"""
}
}
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"
}
}
}

View File

@ -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