Use javascript markdown engine
This commit is contained in:
parent
9f7142b2ab
commit
32b4518bf7
14 changed files with 3908 additions and 105 deletions
|
@ -1,6 +1,6 @@
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
import java.io.FileNotFoundException
|
import java.io.FileNotFoundException
|
||||||
import java.util.*
|
import java.util.Properties
|
||||||
|
|
||||||
plugins {
|
plugins {
|
||||||
id("com.android.application")
|
id("com.android.application")
|
||||||
|
@ -133,7 +133,6 @@ dependencies {
|
||||||
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
|
||||||
implementation("com.google.android.material:material:1.9.0")
|
implementation("com.google.android.material:material:1.9.0")
|
||||||
implementation("androidx.legacy:legacy-support-v13:1.0.0")
|
implementation("androidx.legacy:legacy-support-v13:1.0.0")
|
||||||
implementation("com.commonsware.cwac:anddown:0.4.0")
|
|
||||||
implementation("com.jakewharton.timber:timber:5.0.1")
|
implementation("com.jakewharton.timber:timber:5.0.1")
|
||||||
implementation("androidx.core:core-ktx:1.12.0")
|
implementation("androidx.core:core-ktx:1.12.0")
|
||||||
implementation("androidx.browser:browser:1.6.0")
|
implementation("androidx.browser:browser:1.6.0")
|
||||||
|
|
|
@ -90,11 +90,11 @@ data 1|data 2|data 3
|
||||||
data 1|data 2|data 3
|
data 1|data 2|data 3
|
||||||
```
|
```
|
||||||
|
|
||||||
Left Content|Center Content|Right Content
|
| Left Content | Center Content | Right Content |
|
||||||
:--------|:--------:|--------:
|
|:-------------|:--------------:|--------------:|
|
||||||
data 1|data 2|data 3
|
| data 1 | data 2 | data 3 |
|
||||||
data 1|data 2|data 3
|
| data 1 | data 2 | data 3 |
|
||||||
data 1|data 2|data 3
|
| data 1 | data 2 | data 3 |
|
||||||
|
|
||||||
### Images
|
### Images
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ In addition to the monospace inline element, code blocks can be created by inden
|
||||||
|
|
||||||
Or by wrapping the code in three backticks (\`\`\`):
|
Or by wrapping the code in three backticks (\`\`\`):
|
||||||
|
|
||||||
```
|
```javascript
|
||||||
function helloWorld() {
|
function helloWorld() {
|
||||||
console.log("Hello, world!")
|
console.log("Hello, world!")
|
||||||
}
|
}
|
||||||
|
|
9
app/src/main/assets/highlight.css
Normal file
9
app/src/main/assets/highlight.css
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
/*!
|
||||||
|
Theme: Default
|
||||||
|
Description: Original highlight.js style
|
||||||
|
Author: (c) Ivan Sagalaev <maniac@softwaremaniacs.org>
|
||||||
|
Maintainer: @highlightjs/core-team
|
||||||
|
Website: https://highlightjs.org/
|
||||||
|
License: see project LICENSE
|
||||||
|
Touched: 2021
|
||||||
|
*/pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}.hljs{background:#f3f3f3;color:#444}.hljs-comment{color:#697070}.hljs-punctuation,.hljs-tag{color:#444a}.hljs-tag .hljs-attr,.hljs-tag .hljs-name{color:#444}.hljs-attribute,.hljs-doctag,.hljs-keyword,.hljs-meta .hljs-keyword,.hljs-name,.hljs-selector-tag{font-weight:700}.hljs-deletion,.hljs-number,.hljs-quote,.hljs-selector-class,.hljs-selector-id,.hljs-string,.hljs-template-tag,.hljs-type{color:#800}.hljs-section,.hljs-title{color:#800;font-weight:700}.hljs-link,.hljs-operator,.hljs-regexp,.hljs-selector-attr,.hljs-selector-pseudo,.hljs-symbol,.hljs-template-variable,.hljs-variable{color:#ab5656}.hljs-literal{color:#695}.hljs-addition,.hljs-built_in,.hljs-bullet,.hljs-code{color:#397300}.hljs-meta{color:#1f7199}.hljs-meta .hljs-string{color:#38a}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}
|
1207
app/src/main/assets/highlight.js
Normal file
1207
app/src/main/assets/highlight.js
Normal file
File diff suppressed because one or more lines are too long
14
app/src/main/assets/markdown.js
Normal file
14
app/src/main/assets/markdown.js
Normal file
|
@ -0,0 +1,14 @@
|
||||||
|
const localMarked = new marked.Marked(
|
||||||
|
markedHighlight.markedHighlight({
|
||||||
|
langPrefix: 'hljs language-',
|
||||||
|
highlight(code, lang) {
|
||||||
|
console.log(`highlighing code with $lang`)
|
||||||
|
const language = hljs.getLanguage(lang) ? lang : 'plaintext';
|
||||||
|
return hljs.highlight(code, { language }).value;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
function setMarkdown(markdown) {
|
||||||
|
document.getElementById('content').innerHTML = marked.parse(markdown)
|
||||||
|
}
|
96
app/src/main/assets/marked-highlight.js
Normal file
96
app/src/main/assets/marked-highlight.js
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
(function (global, factory) {
|
||||||
|
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
|
||||||
|
typeof define === 'function' && define.amd ? define(['exports'], factory) :
|
||||||
|
(global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.markedHighlight = {}));
|
||||||
|
})(this, (function (exports) { 'use strict';
|
||||||
|
|
||||||
|
function markedHighlight(options) {
|
||||||
|
if (typeof options === 'function') {
|
||||||
|
options = {
|
||||||
|
highlight: options
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!options || typeof options.highlight !== 'function') {
|
||||||
|
throw new Error('Must provide highlight function');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof options.langPrefix !== 'string') {
|
||||||
|
options.langPrefix = 'language-';
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
async: !!options.async,
|
||||||
|
walkTokens(token) {
|
||||||
|
if (token.type !== 'code') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lang = getLang(token);
|
||||||
|
|
||||||
|
if (options.async) {
|
||||||
|
return Promise.resolve(options.highlight(token.text, lang)).then(updateToken(token));
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = options.highlight(token.text, lang);
|
||||||
|
if (code instanceof Promise) {
|
||||||
|
throw new Error('markedHighlight is not set to async but the highlight function is async. Set the async option to true on markedHighlight to await the async highlight function.');
|
||||||
|
}
|
||||||
|
updateToken(token)(code);
|
||||||
|
},
|
||||||
|
renderer: {
|
||||||
|
code(code, infoString, escaped) {
|
||||||
|
const lang = (infoString || '').match(/\S*/)[0];
|
||||||
|
const classAttr = lang
|
||||||
|
? ` class="${options.langPrefix}${escape(lang)}"`
|
||||||
|
: '';
|
||||||
|
code = code.replace(/\n$/, '');
|
||||||
|
return `<pre><code${classAttr}>${escaped ? code : escape(code, true)}\n</code></pre>`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function getLang(token) {
|
||||||
|
return (token.lang || '').match(/\S*/)[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateToken(token) {
|
||||||
|
return (code) => {
|
||||||
|
if (typeof code === 'string' && code !== token.text) {
|
||||||
|
token.escaped = true;
|
||||||
|
token.text = code;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// copied from marked helpers
|
||||||
|
const escapeTest = /[&<>"']/;
|
||||||
|
const escapeReplace = new RegExp(escapeTest.source, 'g');
|
||||||
|
const escapeTestNoEncode = /[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/;
|
||||||
|
const escapeReplaceNoEncode = new RegExp(escapeTestNoEncode.source, 'g');
|
||||||
|
const escapeReplacements = {
|
||||||
|
'&': '&',
|
||||||
|
'<': '<',
|
||||||
|
'>': '>',
|
||||||
|
'"': '"',
|
||||||
|
"'": '''
|
||||||
|
};
|
||||||
|
const getEscapeReplacement = (ch) => escapeReplacements[ch];
|
||||||
|
function escape(html, encode) {
|
||||||
|
if (encode) {
|
||||||
|
if (escapeTest.test(html)) {
|
||||||
|
return html.replace(escapeReplace, getEscapeReplacement);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (escapeTestNoEncode.test(html)) {
|
||||||
|
return html.replace(escapeReplaceNoEncode, getEscapeReplacement);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.markedHighlight = markedHighlight;
|
||||||
|
|
||||||
|
}));
|
2408
app/src/main/assets/marked.js
Normal file
2408
app/src/main/assets/marked.js
Normal file
File diff suppressed because it is too large
Load diff
|
@ -20,13 +20,13 @@ import androidx.compose.material.icons.filled.Help
|
||||||
import androidx.compose.material.icons.filled.Info
|
import androidx.compose.material.icons.filled.Info
|
||||||
import androidx.compose.material.icons.filled.PrivacyTip
|
import androidx.compose.material.icons.filled.PrivacyTip
|
||||||
import androidx.compose.material.icons.filled.Settings
|
import androidx.compose.material.icons.filled.Settings
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.collectAsState
|
import androidx.compose.runtime.collectAsState
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import androidx.core.app.ActivityCompat
|
import androidx.core.app.ActivityCompat
|
||||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||||
import androidx.core.view.WindowCompat
|
import androidx.core.view.WindowCompat
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.navigation.compose.NavHost
|
import androidx.navigation.compose.NavHost
|
||||||
import androidx.navigation.compose.composable
|
import androidx.navigation.compose.composable
|
||||||
import androidx.navigation.compose.rememberNavController
|
import androidx.navigation.compose.rememberNavController
|
||||||
|
@ -39,42 +39,18 @@ import com.wbrawner.simplemarkdown.ui.SettingsScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
import com.wbrawner.simplemarkdown.ui.SupportScreen
|
||||||
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
import com.wbrawner.simplemarkdown.ui.theme.SimpleMarkdownTheme
|
||||||
import com.wbrawner.simplemarkdown.utility.Preference
|
import com.wbrawner.simplemarkdown.utility.Preference
|
||||||
import kotlinx.coroutines.Dispatchers
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsResultCallback {
|
||||||
private val viewModel: MarkdownViewModel by viewModels { MarkdownViewModel.factory(fileHelper, preferenceHelper) }
|
private val viewModel: MarkdownViewModel by viewModels {
|
||||||
|
MarkdownViewModel.factory(
|
||||||
|
fileHelper,
|
||||||
|
preferenceHelper
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
installSplashScreen()
|
installSplashScreen()
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
lifecycleScope.launch {
|
|
||||||
val darkMode = withContext(Dispatchers.IO) {
|
|
||||||
val darkModeValue = preferenceHelper[Preference.DARK_MODE] as String
|
|
||||||
|
|
||||||
return@withContext when {
|
|
||||||
darkModeValue.equals(
|
|
||||||
getString(R.string.pref_value_light),
|
|
||||||
ignoreCase = true
|
|
||||||
) -> AppCompatDelegate.MODE_NIGHT_NO
|
|
||||||
|
|
||||||
darkModeValue.equals(
|
|
||||||
getString(R.string.pref_value_dark),
|
|
||||||
ignoreCase = true
|
|
||||||
) -> AppCompatDelegate.MODE_NIGHT_YES
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
|
||||||
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
|
||||||
} else {
|
|
||||||
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AppCompatDelegate.setDefaultNightMode(darkMode)
|
|
||||||
}
|
|
||||||
WindowCompat.setDecorFitsSystemWindows(window, false)
|
WindowCompat.setDecorFitsSystemWindows(window, false)
|
||||||
val preferences = mutableMapOf<String, String>()
|
val preferences = mutableMapOf<String, String>()
|
||||||
preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString()
|
preferences["Autosave"] = preferenceHelper[Preference.AUTOSAVE_ENABLED].toString()
|
||||||
|
@ -90,19 +66,45 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
val readabilityEnabled by preferenceHelper.observe<Boolean>(Preference.READABILITY_ENABLED)
|
||||||
.collectAsState()
|
.collectAsState()
|
||||||
|
val darkModePreference by preferenceHelper.observe<String>(Preference.DARK_MODE)
|
||||||
|
.collectAsState()
|
||||||
|
LaunchedEffect(darkModePreference) {
|
||||||
|
val darkMode = when {
|
||||||
|
darkModePreference.equals(
|
||||||
|
getString(R.string.pref_value_light),
|
||||||
|
ignoreCase = true
|
||||||
|
) -> AppCompatDelegate.MODE_NIGHT_NO
|
||||||
|
|
||||||
|
darkModePreference.equals(
|
||||||
|
getString(R.string.pref_value_dark),
|
||||||
|
ignoreCase = true
|
||||||
|
) -> AppCompatDelegate.MODE_NIGHT_YES
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_AUTO_BATTERY
|
||||||
|
} else {
|
||||||
|
AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppCompatDelegate.setDefaultNightMode(darkMode)
|
||||||
|
}
|
||||||
SimpleMarkdownTheme {
|
SimpleMarkdownTheme {
|
||||||
val navController = rememberNavController()
|
val navController = rememberNavController()
|
||||||
NavHost(
|
NavHost(
|
||||||
navController = navController,
|
navController = navController,
|
||||||
startDestination = Route.EDITOR.path,
|
startDestination = Route.EDITOR.path,
|
||||||
enterTransition = { fadeIn(
|
enterTransition = {
|
||||||
animationSpec = tween(
|
fadeIn(
|
||||||
300, easing = LinearEasing
|
animationSpec = tween(
|
||||||
|
300, easing = LinearEasing
|
||||||
|
)
|
||||||
|
) + slideIntoContainer(
|
||||||
|
animationSpec = tween(300, easing = EaseIn),
|
||||||
|
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
||||||
)
|
)
|
||||||
) + slideIntoContainer(
|
},
|
||||||
animationSpec = tween(300, easing = EaseIn),
|
|
||||||
towards = AnimatedContentTransitionScope.SlideDirection.Start
|
|
||||||
) },
|
|
||||||
popEnterTransition = { EnterTransition.None },
|
popEnterTransition = { EnterTransition.None },
|
||||||
popExitTransition = {
|
popExitTransition = {
|
||||||
fadeOut(
|
fadeOut(
|
||||||
|
@ -120,7 +122,8 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
navController = navController,
|
navController = navController,
|
||||||
viewModel = viewModel,
|
viewModel = viewModel,
|
||||||
enableAutosave = autosaveEnabled,
|
enableAutosave = autosaveEnabled,
|
||||||
enableReadability = readabilityEnabled
|
enableReadability = readabilityEnabled,
|
||||||
|
darkMode = darkModePreference
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
composable(Route.SETTINGS.path) {
|
composable(Route.SETTINGS.path) {
|
||||||
|
@ -130,13 +133,25 @@ class MainActivity : AppCompatActivity(), ActivityCompat.OnRequestPermissionsRes
|
||||||
SupportScreen(navController = navController)
|
SupportScreen(navController = navController)
|
||||||
}
|
}
|
||||||
composable(Route.HELP.path) {
|
composable(Route.HELP.path) {
|
||||||
MarkdownInfoScreen(title = Route.HELP.title, file = "Cheatsheet.md", navController = navController)
|
MarkdownInfoScreen(
|
||||||
|
title = Route.HELP.title,
|
||||||
|
file = "Cheatsheet.md",
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(Route.ABOUT.path) {
|
composable(Route.ABOUT.path) {
|
||||||
MarkdownInfoScreen(title = Route.ABOUT.title, file = "Libraries.md", navController = navController)
|
MarkdownInfoScreen(
|
||||||
|
title = Route.ABOUT.title,
|
||||||
|
file = "Libraries.md",
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
composable(Route.PRIVACY.path) {
|
composable(Route.PRIVACY.path) {
|
||||||
MarkdownInfoScreen(title = Route.PRIVACY.title, file = "Privacy Policy.md", navController = navController)
|
MarkdownInfoScreen(
|
||||||
|
title = Route.PRIVACY.title,
|
||||||
|
file = "Privacy Policy.md",
|
||||||
|
navController = navController
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -33,6 +33,12 @@ class MarkdownViewModel(
|
||||||
val effects = _effects.asSharedFlow()
|
val effects = _effects.asSharedFlow()
|
||||||
private val saveMutex = Mutex()
|
private val saveMutex = Mutex()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
load(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
suspend fun updateMarkdown(markdown: String?) {
|
suspend fun updateMarkdown(markdown: String?) {
|
||||||
this@MarkdownViewModel._markdown.emit(markdown ?: "")
|
this@MarkdownViewModel._markdown.emit(markdown ?: "")
|
||||||
isDirty.set(true)
|
isDirty.set(true)
|
||||||
|
|
|
@ -9,10 +9,11 @@ import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
import androidx.compose.foundation.text.BasicTextField
|
|
||||||
import androidx.compose.foundation.text.KeyboardOptions
|
import androidx.compose.foundation.text.KeyboardOptions
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.ArrowBack
|
import androidx.compose.material.icons.filled.ArrowBack
|
||||||
|
@ -38,6 +39,7 @@ import androidx.compose.material3.TabRow
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.material3.TextField
|
import androidx.compose.material3.TextField
|
||||||
|
import androidx.compose.material3.TextFieldDefaults
|
||||||
import androidx.compose.material3.TopAppBar
|
import androidx.compose.material3.TopAppBar
|
||||||
import androidx.compose.material3.rememberDrawerState
|
import androidx.compose.material3.rememberDrawerState
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -51,8 +53,8 @@ import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.SolidColor
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.text.AnnotatedString
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
import androidx.compose.ui.text.SpanStyle
|
import androidx.compose.ui.text.SpanStyle
|
||||||
import androidx.compose.ui.text.TextStyle
|
import androidx.compose.ui.text.TextStyle
|
||||||
|
@ -78,6 +80,7 @@ fun MainScreen(
|
||||||
viewModel: MarkdownViewModel,
|
viewModel: MarkdownViewModel,
|
||||||
enableAutosave: Boolean,
|
enableAutosave: Boolean,
|
||||||
enableReadability: Boolean,
|
enableReadability: Boolean,
|
||||||
|
darkMode: String,
|
||||||
) {
|
) {
|
||||||
var lockSwiping by remember { mutableStateOf(false) }
|
var lockSwiping by remember { mutableStateOf(false) }
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
|
@ -242,8 +245,15 @@ fun MainScreen(
|
||||||
if (page == 0) {
|
if (page == 0) {
|
||||||
TextField(
|
TextField(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize(),
|
||||||
.padding(8.dp),
|
colors = TextFieldDefaults.colors(
|
||||||
|
focusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
unfocusedContainerColor = MaterialTheme.colorScheme.surface,
|
||||||
|
disabledIndicatorColor = Color.Transparent,
|
||||||
|
errorIndicatorColor = Color.Transparent,
|
||||||
|
focusedIndicatorColor = Color.Transparent,
|
||||||
|
unfocusedIndicatorColor = Color.Transparent
|
||||||
|
),
|
||||||
value = textFieldValue,
|
value = textFieldValue,
|
||||||
onValueChange = {
|
onValueChange = {
|
||||||
textFieldValue = if (enableReadability) {
|
textFieldValue = if (enableReadability) {
|
||||||
|
@ -265,7 +275,7 @@ fun MainScreen(
|
||||||
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
|
keyboardOptions = KeyboardOptions(capitalization = KeyboardCapitalization.Sentences)
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown)
|
MarkdownPreview(modifier = Modifier.fillMaxSize(), markdown, darkMode)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -293,6 +303,21 @@ fun MarkdownNavigationDrawer(
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val coroutineScope = rememberCoroutineScope()
|
||||||
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
|
DismissibleNavigationDrawer(drawerState = drawerState, drawerContent = {
|
||||||
DismissibleDrawerSheet {
|
DismissibleDrawerSheet {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
modifier = Modifier.size(96.dp),
|
||||||
|
painter = painterResource(R.drawable.ic_launcher_foreground),
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = "Simple Markdown",
|
||||||
|
style = MaterialTheme.typography.titleLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
Route.entries.forEach { route ->
|
Route.entries.forEach { route ->
|
||||||
if (route == Route.EDITOR) {
|
if (route == Route.EDITOR) {
|
||||||
return@forEach
|
return@forEach
|
||||||
|
|
|
@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.material3.Scaffold
|
import androidx.compose.material3.Scaffold
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.navigation.NavController
|
import androidx.navigation.NavController
|
||||||
|
@ -30,15 +28,16 @@ fun MarkdownInfoScreen(
|
||||||
}
|
}
|
||||||
) { paddingValues ->
|
) { paddingValues ->
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var markdown by remember { mutableStateOf("") }
|
val (markdown, setMarkdown) = remember { mutableStateOf("") }
|
||||||
LaunchedEffect(file) {
|
LaunchedEffect(file) {
|
||||||
markdown = context.assets.readAssetToString(file)?.toHtml()?: "Failed to load $file"
|
setMarkdown(context.assets.readAssetToString(file)?.toHtml() ?: "Failed to load $file")
|
||||||
}
|
}
|
||||||
MarkdownPreview(
|
MarkdownPreview(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.padding(paddingValues),
|
.padding(paddingValues),
|
||||||
markdown = markdown
|
markdown = markdown,
|
||||||
|
"Auto"
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,69 +1,93 @@
|
||||||
package com.wbrawner.simplemarkdown.ui
|
package com.wbrawner.simplemarkdown.ui
|
||||||
|
|
||||||
import android.content.res.Configuration
|
import android.content.Context
|
||||||
|
import android.view.ViewGroup
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.foundation.layout.fillMaxSize
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.rememberCoroutineScope
|
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.wbrawner.simplemarkdown.BuildConfig
|
import com.wbrawner.simplemarkdown.BuildConfig
|
||||||
import com.wbrawner.simplemarkdown.R
|
|
||||||
import com.wbrawner.simplemarkdown.utility.toHtml
|
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.Reader
|
||||||
|
|
||||||
|
private const val container = "<main id=\"content\"></main>"
|
||||||
|
|
||||||
|
@OptIn(ExperimentalStdlibApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String) {
|
fun MarkdownPreview(modifier: Modifier = Modifier, markdown: String, darkMode: String) {
|
||||||
val coroutineScope = rememberCoroutineScope()
|
val materialColors = MaterialTheme.colorScheme
|
||||||
|
val style = remember(darkMode) {
|
||||||
|
"""body {
|
||||||
|
| background: #${materialColors.surface.toArgb().toHexString().substring(2)};
|
||||||
|
| color: #${materialColors.onSurface.toArgb().toHexString().substring(2)};
|
||||||
|
|}
|
||||||
|
|pre {
|
||||||
|
| background: #${materialColors.surfaceVariant.toArgb().toHexString().substring(2)};
|
||||||
|
| color: #${materialColors.onSurfaceVariant.toArgb().toHexString().substring(2)};
|
||||||
|
|}""".trimMargin().wrapTag("style")
|
||||||
|
}
|
||||||
|
var marked by remember { mutableStateOf("") }
|
||||||
|
var markedHighlight by remember { mutableStateOf("") }
|
||||||
|
var highlightJs by remember { mutableStateOf("") }
|
||||||
|
var highlightCss by remember { mutableStateOf("") }
|
||||||
|
var markdownJs by remember { mutableStateOf("") }
|
||||||
|
val markdownUpdateJs by remember(markdown) {
|
||||||
|
mutableStateOf(
|
||||||
|
"setMarkdown(`${
|
||||||
|
markdown.replace(
|
||||||
|
"`",
|
||||||
|
"\\`"
|
||||||
|
)
|
||||||
|
}`)".wrapTag("script")
|
||||||
|
)
|
||||||
|
}
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
var style by remember { mutableStateOf("") }
|
|
||||||
LaunchedEffect(context) {
|
LaunchedEffect(context) {
|
||||||
val isNightMode = AppCompatDelegate.getDefaultNightMode() ==
|
withContext(Dispatchers.IO) {
|
||||||
AppCompatDelegate.MODE_NIGHT_YES
|
marked = context.assetToString("marked.js").wrapTag("script")
|
||||||
|| context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES
|
markedHighlight = context.assetToString("marked-highlight.js").wrapTag("script")
|
||||||
val defaultCssId = if (isNightMode) {
|
highlightJs = context.assetToString("highlight.js").wrapTag("script")
|
||||||
R.string.pref_custom_css_default_dark
|
highlightCss = context.assetToString("highlight.css").wrapTag("style")
|
||||||
} else {
|
markdownJs = context.assetToString("markdown.js").wrapTag("script")
|
||||||
R.string.pref_custom_css_default
|
|
||||||
}
|
}
|
||||||
val css = withContext(Dispatchers.IO) {
|
|
||||||
@Suppress("ConstantConditionIf")
|
|
||||||
if (!BuildConfig.ENABLE_CUSTOM_CSS) {
|
|
||||||
context.getString(defaultCssId)
|
|
||||||
} else {
|
|
||||||
PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getString(
|
|
||||||
context.getString(R.string.pref_custom_css),
|
|
||||||
context.getString(defaultCssId)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
style = "<style>$css</style>"
|
|
||||||
}
|
}
|
||||||
AndroidView(
|
AndroidView(
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
factory = { context ->
|
factory = { context ->
|
||||||
WebView(context)
|
val content =
|
||||||
},
|
highlightCss + style + container + marked + markedHighlight + highlightJs + markdownJs + markdownUpdateJs
|
||||||
update = { preview ->
|
|
||||||
coroutineScope.launch {
|
WebView(context).apply {
|
||||||
preview.loadDataWithBaseURL(null,
|
WebView.setWebContentsDebuggingEnabled(BuildConfig.DEBUG)
|
||||||
style + markdown.toHtml(),
|
layoutParams = ViewGroup.LayoutParams(
|
||||||
"text/html",
|
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||||
"UTF-8", null
|
ViewGroup.LayoutParams.WRAP_CONTENT
|
||||||
)
|
)
|
||||||
preview.setBackgroundColor(0x01000000)
|
setBackgroundColor(Color.Transparent.toArgb())
|
||||||
|
isNestedScrollingEnabled = false
|
||||||
|
settings.javaScriptEnabled = true
|
||||||
|
loadDataWithBaseURL(null, content, "text/html", "UTF-8", null)
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
update = { webView ->
|
||||||
|
val content =
|
||||||
|
highlightCss + style + container + marked + markedHighlight + highlightJs + markdownJs + markdownUpdateJs
|
||||||
|
webView.loadDataWithBaseURL(null, content, "text/html", "UTF-8", null)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun String.wrapTag(tag: String) = "<$tag>$this</$tag>"
|
||||||
|
|
||||||
|
private fun Context.assetToString(fileName: String): String =
|
||||||
|
assets.open(fileName).reader().use(Reader::readText)
|
|
@ -1,7 +1,7 @@
|
||||||
package com.wbrawner.simplemarkdown.utility
|
package com.wbrawner.simplemarkdown.utility
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.net.Uri
|
import android.content.Intent
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.withContext
|
import kotlinx.coroutines.withContext
|
||||||
|
@ -36,6 +36,10 @@ class AndroidFileHelper(private val context: Context) : FileHelper {
|
||||||
|
|
||||||
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
override suspend fun open(source: URI): Pair<String, String>? = withContext(Dispatchers.IO) {
|
||||||
val uri = source.toString().toUri()
|
val uri = source.toString().toUri()
|
||||||
|
context.contentResolver.takePersistableUriPermission(
|
||||||
|
uri,
|
||||||
|
Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
|
)
|
||||||
context.contentResolver.openFileDescriptor(uri, "r")
|
context.contentResolver.openFileDescriptor(uri, "r")
|
||||||
?.use {
|
?.use {
|
||||||
uri.getName(context) to FileInputStream(it.fileDescriptor).reader()
|
uri.getName(context) to FileInputStream(it.fileDescriptor).reader()
|
||||||
|
|
|
@ -17,9 +17,6 @@ allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
mavenCentral()
|
mavenCentral()
|
||||||
maven {
|
|
||||||
url = uri("https://s3.amazonaws.com/repo.commonsware.com/")
|
|
||||||
}
|
|
||||||
maven {
|
maven {
|
||||||
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
url = uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue