From fa6e379b08475c88c081e73c50012fb22c6cde1f Mon Sep 17 00:00:00 2001 From: Dimo Karaivanov Date: Mon, 5 Aug 2024 15:55:41 +0300 Subject: [PATCH] Real help screen (#589) * the manual is converted from markdown to HTML during build time * The user manual is now included in the application. The Help section will no longer attempt to open it from Github * fixed punctuation mistakes in the manual --- app/build.gradle | 12 +- app/constants.gradle | 3 + app/help-tools.gradle | 166 ++++++++++++++++++ app/src/main/AndroidManifest.xml | 14 +- .../sspanak/tt9/preferences/HelpActivity.java | 76 ++++++++ .../screens/main/MainSettingsScreen.java | 25 --- app/src/main/res/values/strings.xml | 1 - app/src/main/res/xml/prefs.xml | 7 +- docs/user-manual.md | 6 +- 9 files changed, 275 insertions(+), 35 deletions(-) create mode 100644 app/help-tools.gradle create mode 100644 app/src/main/java/io/github/sspanak/tt9/preferences/HelpActivity.java diff --git a/app/build.gradle b/app/build.gradle index 6d581ae9..8dcb5240 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -4,6 +4,7 @@ plugins { apply from: 'constants.gradle' apply from: 'dictionary-tools.gradle' +apply from: 'help-tools.gradle' apply from: 'validate-languages.gradle' apply from: 'version-tools.gradle' @@ -30,6 +31,15 @@ tasks.register('copyDictionaries', Copy) { into DICTIONARIES_OUTPUT_DIR } +tasks.register('convertHelp') { + inputs.file HELP_MARKDOWN + outputs.file HELP_HTML + + doLast { + markdownToHTML(HELP_MARKDOWN, HELP_HTML, getVersionName()) + } +} + tasks.register('writeDictionaryProperties') { inputs.dir fileTree(dir: DICTIONARIES_INPUT_DIR) outputs.dir DICTIONARY_META_OUTPUT_DIR @@ -107,7 +117,7 @@ android { ].each { taskName -> try { tasks.named(taskName)?.configure { - dependsOn(validateLanguages, copyDefinitions, copyDictionaries, writeDictionaryProperties) + dependsOn(validateLanguages, copyDefinitions, copyDictionaries, writeDictionaryProperties, convertHelp) } } catch (UnknownTaskException ignored) {} } diff --git a/app/constants.gradle b/app/constants.gradle index b715549d..a72b130c 100644 --- a/app/constants.gradle +++ b/app/constants.gradle @@ -10,6 +10,9 @@ def ROOT_DIR = "${project.rootDir}/app" def MAIN_ASSETS_DIR = "${ROOT_DIR}/src/main/assets" def FULL_VERSION_ASSETS_DIR = "${ROOT_DIR}/src/full/assets" +ext.HELP_MARKDOWN = "${project.rootDir}/docs/user-manual.md" +ext.HELP_HTML = "${MAIN_ASSETS_DIR}/help.html" + ext.LANGUAGES_INPUT_DIR = "${ROOT_DIR}/${LANGUAGES_DIR_NAME}" ext.DEFINITIONS_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DEFINITIONS_DIR_NAME}" ext.DICTIONARIES_INPUT_DIR = "${LANGUAGES_INPUT_DIR}/${DICTIONARIES_DIR_NAME}" diff --git a/app/help-tools.gradle b/app/help-tools.gradle new file mode 100644 index 00000000..12b3a90f --- /dev/null +++ b/app/help-tools.gradle @@ -0,0 +1,166 @@ +ext.markdownToHTML = { markdownPath, htmlPath, version -> + def text = new File(markdownPath).text + + text = convertHeaders(text, version) + text = convertOrderedLists(text) + text = convertUnorderedLists(text) + text = convertInlineTags(text) + text = addStylesToTags(text) + text = insertIndex(text, generateIndex(text)) + text = removeWhitespace(text) + + new File(htmlPath).text = "Help${text}" +} + + +static getStyles() { + return "body {padding: 0 6px; background-color: #f4f4f4; color: #000;}" + + "a {color: #225682}" + + "a:visited {color: #644280}" + + "li {margin: 4px 0; padding: 1px;}" + + "p {text-align: justify;}" + + "p.wrap{word-wrap: break-word;}" + + ".toc {border: 1px solid; display: inline-block; padding: 12px 20px 12px 0; margin: 12px 0;}" + + ".toc > h3 {text-align: center; margin: 0;}" + + "@media (prefers-color-scheme: dark) {" + + "body { background-color: #333; color: #c8c8c8; }" + + "a {color: #a0c1de}" + + "a:visited {color: #d9bce1}" + + "}" +} + + +static generateIndex(html) { + def entries = html.split("\n").collect( { line -> + def matches = line =~ "

(.+)

" + if (matches.size() > 0 && matches[0].size() > 2) { + return "${matches[0][2]}" + } else { + return null + } + }).findAll { it != null } + + return "

Contents

" + + "
    ${entries.collect { "
  1. ${it}
  2. " }.join("\n")}
" + + "
" +} + + +static insertIndex(html, index) { + return html.replaceFirst(" + if (line.startsWith("#")) { + def headerNumber = 0 + for (int i = 0; i < line.length(); i++) { + if (line[i] != '#') { + headerNumber = i + break + } + } + + def header = line.replaceAll("^#+", "").trim() + def anchor = header.toLowerCase().replaceAll("[^a-z0-9]+", "-") + + return "" + + "${header}${headerNumber == 1 ? " v" + version : ''}" + + "" + } else { + return line + } + } + + return html.join("\n") +} + + +static convertOrderedLists(markdown) { + def html = markdown.split("\n").collect { line -> + if (line.matches("^\\d+\\..*")) { + return "
  • ${line.replaceAll("^\\d+\\.\\s*", "")}
  • " + } else { + return line + } + } + + return html.join("\n").replaceAll("(?\n)
  • ", "
    1. ").replaceAll("
    2. (?!\n
    ") +} + + +static convertUnorderedLists(markdown) { + boolean inList = false + boolean inNestedList = false + + def html = "" + + markdown.split("\n").each { line -> + def convertedLine = "" + + def innerLi = line.replaceAll("^\\s*-\\s*", "") + + if (line.matches("^-.*")) { + if (!inList) { + convertedLine += "
      " + inList = true + } + + if (inNestedList) { + convertedLine += "
  • " + inNestedList = false + } + + convertedLine += "
  • ${innerLi}
  • " + } else if (line.matches("^\\s+-.*")) { + if (!inNestedList) { + if (html.endsWith("")) { + html = html.substring(0, html.length() - 5) + } else if (html.endsWith("\n")) { + html = html.substring(0, html.length() - 6) + } + + convertedLine += "" + } + + if (inList) { + inList = false + convertedLine += "" + } + + convertedLine += line + } + + html += convertedLine + "\n" + } + + return html +} + + +static convertInlineTags(markdown) { + return markdown + .replaceAll("\n([^\n<]+?)(\n|\$)", "

    \$1

    ") + .replaceAll("_([^_]+)_", "\$1") + .replaceAll("[*]{2}(.+?)[*]{2}", "\$1") + .replaceAll("\\[([^]]+)\\]\\(([^)]+)\\)", "\$1") +} + + +static addStylesToTags(html) { + return html.replaceAll("

    ([^<]+?googlequicksearchbox[^<]+?)

    ", "

    \$1

    ") +} + + +static removeWhitespace(html) { + return html.replaceAll("\\s+", " ").replaceAll("/> <", "/><") +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 0b789cb5..47b0312e 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,7 +28,9 @@ - @@ -42,6 +44,16 @@ android:name="io.github.sspanak.tt9.ui.dialogs.PopupDialogActivity" android:theme="@style/alertDialog" /> + + + + + + + 30 the WebView does not load the entire String with .loadData(), + // so we need to do this weird shit. + // The "app:" prefix is mandatory, otherwise the anchor links do not work. + // Reference: https://developer.android.com/develop/ui/views/layout/webapps/webview + String html = getHelpHtml(); + String encodedHtml = "app:" + Base64.encodeToString(html.getBytes(), Base64.NO_PADDING); + container.loadDataWithBaseURL(encodedHtml, html, "text/html", "UTF-8", null); + + setContentView(container); + } + + private String getHelpHtml() { + AssetManager assets = getAssets(); + try { + InputStream stream = assets.open("help.html"); + BufferedReader reader = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8)); + StringBuilder builder = new StringBuilder(); + String line; + while ((line = reader.readLine()) != null) { + builder.append(line); + } + return builder.toString(); + } catch (Exception e) { + Logger.e(getClass().getSimpleName(), "Failed opening the help.html file."); + return ""; + } + } +} diff --git a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java index ae3c8018..55f4e70d 100644 --- a/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java +++ b/app/src/main/java/io/github/sspanak/tt9/preferences/screens/main/MainSettingsScreen.java @@ -31,7 +31,6 @@ public class MainSettingsScreen extends BaseScreenFragment { @Override public void onCreate() { createSettingsSection(); - addHelpLink(); createAboutSection(); resetFontSize(false); } @@ -46,30 +45,6 @@ public class MainSettingsScreen extends BaseScreenFragment { } - private void addHelpLink() { - try { - if (!releaseVersionRegex.matcher(BuildConfig.VERSION_NAME).find()) { - throw new Exception("VERSION_NAME does not match: \\d+.\\d+"); - } - - Preference helpSection = findPreference("help"); - if (helpSection == null) { - throw new Exception("Could not find Help Preference"); - } - - String majorVersion = BuildConfig.VERSION_NAME.substring(0, BuildConfig.VERSION_NAME.indexOf('.')); - String versionedHelpUrl = getString(R.string.help_url).replace("blob/master", "blob/v" + majorVersion + ".0"); - - Intent intent = new Intent(); - intent.setAction("android.intent.action.VIEW"); - intent.setData(Uri.parse(versionedHelpUrl)); - helpSection.setIntent(intent); - } catch (Exception e) { - Logger.w("MainSettingsScreen", "Could not set versioned help URL. Falling back to the default. " + e.getMessage()); - } - } - - private void createAboutSection() { Preference donate = findPreference("donate_link"); if (donate != null) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4398e4f8..2ed9f21b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,7 +1,6 @@ https://raw.githubusercontent.com/sspanak/tt9/%1$s/app/%2$s - https://github.com/sspanak/tt9/blob/master/docs/user-manual.md Traditional T9 TT9 TT9 Settings diff --git a/app/src/main/res/xml/prefs.xml b/app/src/main/res/xml/prefs.xml index 7f631351..bac5506a 100644 --- a/app/src/main/res/xml/prefs.xml +++ b/app/src/main/res/xml/prefs.xml @@ -4,12 +4,13 @@ app:orderingFromXml="true"> + android:targetPackage="io.github.sspanak.tt9" + android:targetClass="io.github.sspanak.tt9.preferences.HelpActivity" />