アプリ設定に公開範囲別のトゥート背景色を追加

This commit is contained in:
tateisu 2018-08-24 14:04:55 +09:00
parent 4ae72558b0
commit b7f5f67520
13 changed files with 687 additions and 444 deletions

View File

@ -72,6 +72,11 @@ class ActAppSetting : AppCompatActivity()
internal const val COLOR_DIALOG_ID_FOOTER_TAB_INDICATOR = 5
internal const val COLOR_DIALOG_ID_LIST_DIVIDER = 6
internal const val COLOR_DIALOG_ID_TOOT_BG_UNLISTED = 7
internal const val COLOR_DIALOG_ID_TOOT_BG_FOLLOWER = 8
internal const val COLOR_DIALOG_ID_TOOT_BG_DIRECT_USER = 9
internal const val COLOR_DIALOG_ID_TOOT_BG_DIRECT_ME = 10
internal const val REQUEST_CODE_TIMELINE_FONT = 1
internal const val REQUEST_CODE_TIMELINE_FONT_BOLD = 2
internal const val REQUEST_CODE_APP_DATA_EXPORT = 3
@ -113,6 +118,10 @@ class ActAppSetting : AppCompatActivity()
private var footer_tab_divider_color : Int = 0
private var footer_tab_indicator_color : Int = 0
private var list_divider_color : Int = 0
private var toot_color_unlisted : Int = 0
private var toot_color_follower : Int = 0
private var toot_color_direct_user : Int = 0
private var toot_color_direct_me : Int = 0
private lateinit var ivFooterToot : ImageView
private lateinit var ivFooterMenu : ImageView
@ -267,6 +276,16 @@ class ActAppSetting : AppCompatActivity()
findViewById<View>(R.id.btnListDividerColorEdit).setOnClickListener(this)
findViewById<View>(R.id.btnListDividerColorReset).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorUnlistedEdit).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorUnlistedReset).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorFollowerEdit).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorFollowerReset).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorDirectWithUserEdit).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorDirectWithUserReset).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorDirectNoUserEdit).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorDirectNoUserReset).setOnClickListener(this)
findViewById<View>(R.id.btnTimelineFontEdit).setOnClickListener(this)
findViewById<View>(R.id.btnTimelineFontReset).setOnClickListener(this)
findViewById<View>(R.id.btnTimelineFontBoldEdit).setOnClickListener(this)
@ -391,6 +410,10 @@ class ActAppSetting : AppCompatActivity()
footer_tab_divider_color = Pref.ipFooterTabDividerColor(pref)
footer_tab_indicator_color = Pref.ipFooterTabIndicatorColor(pref)
list_divider_color = Pref.ipListDividerColor(pref)
toot_color_unlisted = Pref.ipTootColorUnlisted(pref)
toot_color_follower = Pref.ipTootColorFollower(pref)
toot_color_direct_user = Pref.ipTootColorDirectUser(pref)
toot_color_direct_me = Pref.ipTootColorDirectMe(pref)
etColumnWidth.setText(Pref.spColumnWidth(pref))
etMediaThumbHeight.setText(Pref.spMediaThumbHeight(pref))
@ -498,6 +521,11 @@ class ActAppSetting : AppCompatActivity()
.put(Pref.ipFooterTabDividerColor, footer_tab_divider_color)
.put(Pref.ipFooterTabIndicatorColor, footer_tab_indicator_color)
.put(Pref.ipListDividerColor, list_divider_color)
.put(Pref.ipTootColorUnlisted, toot_color_unlisted)
.put(Pref.ipTootColorFollower, toot_color_follower)
.put(Pref.ipTootColorDirectUser, toot_color_direct_user)
.put(Pref.ipTootColorDirectMe, toot_color_direct_me)
.apply()
@ -592,10 +620,50 @@ class ActAppSetting : AppCompatActivity()
true
)
R.id.btnBackgroundColorUnlistedEdit -> openColorPicker(
COLOR_DIALOG_ID_TOOT_BG_UNLISTED,
toot_color_unlisted,
true
)
R.id.btnBackgroundColorFollowerEdit -> openColorPicker(
COLOR_DIALOG_ID_TOOT_BG_FOLLOWER,
toot_color_follower,
true
)
R.id.btnBackgroundColorDirectWithUserEdit -> openColorPicker(
COLOR_DIALOG_ID_TOOT_BG_DIRECT_USER,
toot_color_direct_user,
true
)
R.id.btnBackgroundColorDirectNoUserEdit -> openColorPicker(
COLOR_DIALOG_ID_TOOT_BG_DIRECT_ME,
toot_color_direct_me,
true
)
R.id.btnListDividerColorReset -> {
list_divider_color = 0
saveUIToData()
}
R.id.btnBackgroundColorUnlistedReset -> {
toot_color_unlisted = 0
saveUIToData()
}
R.id.btnBackgroundColorFollowerReset -> {
toot_color_follower = 0
saveUIToData()
}
R.id.btnBackgroundColorDirectWithUserReset -> {
toot_color_direct_user = 0
saveUIToData()
}
R.id.btnBackgroundColorDirectNoUserReset -> {
toot_color_direct_me = 0
saveUIToData()
}
R.id.btnTimelineFontReset -> {
timeline_font = ""
@ -723,6 +791,24 @@ class ActAppSetting : AppCompatActivity()
list_divider_color = if(colorSelected == 0) 0x01000000 else colorSelected
saveUIToData()
}
COLOR_DIALOG_ID_TOOT_BG_UNLISTED -> {
toot_color_unlisted = if(colorSelected == 0) 0x01000000 else colorSelected
saveUIToData()
}
COLOR_DIALOG_ID_TOOT_BG_FOLLOWER -> {
toot_color_follower = if(colorSelected == 0) 0x01000000 else colorSelected
saveUIToData()
}
COLOR_DIALOG_ID_TOOT_BG_DIRECT_USER -> {
toot_color_direct_user= if(colorSelected == 0) 0x01000000 else colorSelected
saveUIToData()
}
COLOR_DIALOG_ID_TOOT_BG_DIRECT_ME -> {
toot_color_direct_me = if(colorSelected == 0) 0x01000000 else colorSelected
saveUIToData()
}
}
}

View File

@ -461,6 +461,10 @@ class ActMain : AppCompatActivity()
// カラーカスタマイズを読み直す
ListDivider.color = Pref.ipListDividerColor(pref)
ItemViewHolder.toot_color_unlisted = Pref.ipTootColorUnlisted(pref)
ItemViewHolder.toot_color_follower = Pref.ipTootColorFollower(pref)
ItemViewHolder.toot_color_direct_user = Pref.ipTootColorDirectUser(pref)
ItemViewHolder.toot_color_direct_me = Pref.ipTootColorDirectMe(pref)
// アカウント設定から戻ってきたら、カラムを消す必要があるかもしれない
run {

View File

@ -43,6 +43,10 @@ internal class ItemViewHolder(
companion object {
private val log = LogCategory("ItemViewHolder")
var toot_color_unlisted : Int =0
var toot_color_follower : Int =0
var toot_color_direct_user : Int =0
var toot_color_direct_me : Int =0
}
val viewRoot : View
@ -757,6 +761,17 @@ internal class ItemViewHolder(
R.attr.colorImageButtonAccent
) and 0xffffff) or 0x20000000
)
}else{
val c = when(status.getBackgroundColorType(access_info)){
TootVisibility.UnlistedHome-> toot_color_unlisted
TootVisibility.PrivateFollowers-> toot_color_follower
TootVisibility.DirectSpecified-> toot_color_direct_user
TootVisibility.DirectPrivate-> toot_color_direct_me
else->0
}
if(c!=0){
this.viewRoot.setBackgroundColor(c)
}
}
showStatusTime(activity, tvTime, who = status.account, status = status)

View File

@ -343,7 +343,12 @@ object Pref {
val ipLastColumnPos = IntPref("last_column_pos", - 1)
val ipBoostButtonJustify = IntPref("ipBoostButtonJustify", 0) // 0=左,1=中央,2=右
// val ipTrendTagCountShowing = IntPref("TrendTagCountShowing", 0)
val ipTootColorUnlisted = IntPref("ipTootColorUnlisted", 0)
val ipTootColorFollower = IntPref("ipTootColorFollower", 0)
val ipTootColorDirectUser = IntPref("ipTootColorDirectUser", 0)
val ipTootColorDirectMe = IntPref("ipTootColorDirectMe", 0)
// val ipTrendTagCountShowing = IntPref("TrendTagCountShowing", 0)
// const val TTCS_WEEKLY = 0
// const val TTCS_DAILY = 1

View File

@ -500,6 +500,32 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
val filtered : Boolean
get() = _filtered || reblog?._filtered == true
private fun hasReceipt(access_info:SavedAccount):TootVisibility{
val reply = this.reply
if( reply != null && ! access_info.isMe(reply.account)) {
return TootVisibility.DirectSpecified
}
val in_reply_to_account_id = this.in_reply_to_account_id
if( in_reply_to_account_id != null && in_reply_to_account_id != access_info.loginAccount?.id) {
return TootVisibility.DirectSpecified
}
mentions?.forEach{
if(!access_info.isMe(it.acct))
return@hasReceipt TootVisibility.DirectSpecified
}
return TootVisibility.DirectPrivate
}
fun getBackgroundColorType(access_info:SavedAccount) =
when(visibility){
TootVisibility.DirectPrivate,
TootVisibility.DirectSpecified -> hasReceipt(access_info)
else-> visibility
}
fun updateFiltered(muted_words : WordTrieTree?) {
_filtered = checkFiltered(muted_words)

View File

@ -31,15 +31,14 @@ private fun String.safeSubstring(count : Int, offset : Int = 0) = when {
else -> this.substring(offset, length)
}
// 配列中の要素をラムダ式で変換して、戻り値が非nullならそこで処理を打ち切る
private inline fun <T, V> Array<out T>.firstNonNull(predicate : (T) -> V?) : V? {
for(element in this) return predicate(element) ?: continue
return null
}
object MisskeySyntaxHighlighter {
private val symbolMap = SparseBooleanArray().apply {
for(c in "=+-*/%~^&|><!?") {
this.put(c.toInt(), true)
}
}
// 識別子に対して既存の名前と一致するか調べるようになったので、もはやソートの必要はない
private val keywords = HashSet<String>().apply {
val _keywords = arrayOf(
@ -134,20 +133,100 @@ object MisskeySyntaxHighlighter {
addAll(_keywords.map { k -> k[0].toUpperCase() + k.substring(1) })
add("NaN")
// 識別子に対して既存の名前と一致するか調べるようになったので、もはやソートの必要はない
}
private val symbolMap = SparseBooleanArray().apply {
for(c in "=+-*/%~^&|><!?") {
this.put(c.toInt(), true)
}
}
private val stringStart = SparseBooleanArray().apply {
for(c in "\"'`") {
this.put(c.toInt(), true)
}
}
private class Token(
var length : Int,
var color : Int = 0,
val length : Int,
val color : Int = 0,
val italic : Boolean = false,
val comment : Boolean = false
)
private class Env(
var source : String,
var pos : Int,
var remain : String
)
private class Env(val source : String) {
// 出力先
val sb = SpannableStringBuilder(source)
// 残り部分
var remain : String = source
private set
// スキャン位置
var pos : Int = 0
set(value) {
field = value
remain = source.substring(value)
}
fun push(start : Int, token : Token) {
val end = start + token.length
if(token.comment) {
sb.setSpan(
ForegroundColorSpan(Color.BLACK or 0x808000)
, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
} else {
var c = token.color
if(c != 0) {
if(c < 0x1000000) {
c = c or Color.BLACK
}
sb.setSpan(
ForegroundColorSpan(c)
, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
if(token.italic) {
sb.setSpan(
CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC))
, start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
fun parse() : SpannableStringBuilder {
var lastEnd = 0
fun closeTextToken(textEnd : Int) {
val length = textEnd - lastEnd
if(length > 0) {
push(lastEnd, Token(length = length))
lastEnd = textEnd
}
}
while(remain.isNotEmpty()) {
val token = elements.firstNonNull { this.it() }
if(token == null) {
++ pos
} else {
closeTextToken(pos)
push(pos, token)
this.pos += token.length
lastEnd = pos
}
}
closeTextToken(pos)
return sb
}
}
private val reLineComment = Pattern.compile("""\A//.*""")
private val reBlockComment = Pattern.compile("""\A/\*.*?\*/""", Pattern.DOTALL)
@ -156,39 +235,39 @@ object MisskeySyntaxHighlighter {
private val reKeyword =
Pattern.compile("""\A([A-Z_-][A-Z0-9_-]*)([ \t]*\()?""", Pattern.CASE_INSENSITIVE)
private val elements = arrayOf(
private val elements = arrayOf<Env.() -> Token?>(
// comment
{ env : Env ->
val match = reLineComment.matcher(env.remain)
{
val match = reLineComment.matcher(remain)
when {
match.find() -> Token(length = match.end(), comment = true)
else -> null
! match.find() -> null
else -> Token(length = match.end(), comment = true)
}
},
// block comment
{ env : Env ->
val match = reBlockComment.matcher(env.remain)
{
val match = reBlockComment.matcher(remain)
when {
match.find() -> Token(length = match.end(), comment = true)
else -> null
! match.find() -> null
else -> Token(length = match.end(), comment = true)
}
},
// string
{ env : Env ->
val beginChar = env.remain[0]
if(beginChar != '"' && beginChar != '`') return@arrayOf null
{
val beginChar = remain[0]
if(!stringStart[beginChar.toInt()]) return@arrayOf null
var len = 1
while(len < env.remain.length) {
val char = env.remain[len ++]
while(len < remain.length) {
val char = remain[len ++]
if(char == beginChar) {
break // end
} else if(char == '\n' || len >= env.remain.length) {
} else if(char == '\n' || len >= remain.length) {
len = 0 // not string literal
break
} else if(char == '\\' && len < env.remain.length) {
} else if(char == '\\' && len < remain.length) {
++ len // \" では閉じないようにする
}
}
@ -199,27 +278,27 @@ object MisskeySyntaxHighlighter {
},
// regexp
{ env : Env ->
if(env.remain[0] != '/') return@arrayOf null
{
if(remain[0] != '/') return@arrayOf null
val regexp = StringBuilder()
var thisIsNotARegexp = false
var notClosed = false
var i = 1
while(i < env.remain.length) {
val char = env.remain[i ++]
while(i < remain.length) {
val char = remain[i ++]
if(char == '/') {
break
} else if(char == '\n' || i >= env.remain.length) {
thisIsNotARegexp = true
} else if(char == '\n' || i >= remain.length) {
notClosed = true
break
} else {
regexp.append(char)
if(char == '\\' && i < env.remain.length) {
regexp.append(env.remain[i ++])
if(char == '\\' && i < remain.length) {
regexp.append(remain[i ++])
}
}
}
when {
thisIsNotARegexp -> null
notClosed -> null
regexp.isEmpty() -> null
regexp[0] == ' ' && regexp[regexp.length - 1] == ' ' -> null
else -> Token(length = regexp.length + 2, color = 0xe9003f)
@ -227,53 +306,57 @@ object MisskeySyntaxHighlighter {
},
// label
{ env : Env ->
{
// 直前に識別子があればNG
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
val prev = if(pos <= 0) null else source[pos - 1]
if(prev?.isLetterOrDigit() == true) return@arrayOf null
val match = reLabel.matcher(env.remain)
val match = reLabel.matcher(remain)
if(! match.find()) return@arrayOf null
val end = match.end()
when {
// @user@host のように直後に@が続くのはNG
env.remain.length > end && env.remain[end] == '@' -> null
remain.length > end && remain[end] == '@' -> null
else -> Token(length = match.end(), color = 0xe9003f)
}
},
// number
{ env : Env ->
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
{
val prev = if(pos <= 0) null else source[pos - 1]
if(prev?.isLetterOrDigit() == true) return@arrayOf null
val match = reNumber.matcher(env.remain)
val match = reNumber.matcher(remain)
when {
match.find() -> Token(length = match.end(), color = 0xae81ff)
else -> null
! match.find() -> null
else -> Token(length = match.end(), color = 0xae81ff)
}
},
// method, property, keyword
{ env : Env ->
{
// 直前の文字が識別子に使えるなら識別子の開始とはみなさない
val prev = if(env.pos <= 0) null else env.source[env.pos - 1]
val prev = if(pos <= 0) null else source[pos - 1]
if(prev?.isLetterOrDigit() == true || prev == '_') return@arrayOf null
val match = reKeyword.matcher(env.remain)
val match = reKeyword.matcher(remain)
if(! match.find()) return@arrayOf null
val kw = match.group(1)
val bracket = match.group(2)
when {
// メソッド呼び出しは対が変数かプロパティかに関わらずメソッドの色になる
// メソッド呼び出しは対が変数かプロパティかに関わらずメソッドの色になる
bracket?.isNotEmpty() == true ->
Token(length = kw.length, color = 0x8964c1, italic = true)
// 変数や定数ではなくプロパティならプロパティの色になる
prev == '.' -> Token(length = kw.length, color = 0xa71d5d)
keywords.contains(kw) -> when(kw) {
// 予約語ではない
// 強調表示しないが、識別子単位で読み飛ばす
! keywords.contains(kw) -> Token(length = kw.length)
else -> when(kw) {
// 定数
"true", "false", "null", "nil", "undefined", "NaN" ->
@ -282,86 +365,21 @@ object MisskeySyntaxHighlighter {
// その他の予約語
else -> Token(length = kw.length, color = 0x2973b7)
}
// 強調表示しないが、識別子単位で読み飛ばす
else -> Token(length = kw.length)
}
},
// symbol
{ env : Env ->
{
when {
symbolMap.get(env.remain[0].toInt(), false) ->
symbolMap.get(remain[0].toInt(), false) ->
Token(length = 1, color = 0x42b983)
else ->
null
else -> null
}
}
)
fun parse(source : String) : SpannableStringBuilder {
val sb = SpannableStringBuilder()
val env = Env(source = source, pos = 0, remain = source)
fun push(pos : Int, token : Token) {
val end = pos + token.length
sb.append(source.substring(pos, end))
env.pos = end
if(token.comment) {
sb.setSpan(
ForegroundColorSpan(Color.BLACK or 0x808000)
, pos, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
} else {
var c = token.color
if(c != 0) {
if(c < 0x1000000) {
c = c or Color.BLACK
}
sb.setSpan(
ForegroundColorSpan(c)
, pos, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
if(token.italic) {
sb.setSpan(
CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC))
, pos, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
var textToken : Token? = null
var textTokenStart = 0
fun closeTextToken() {
val token = textToken
if(token != null) {
token.length = env.pos - textTokenStart
push(textTokenStart, token)
textToken = null
}
}
loop1@ while(env.remain.isNotEmpty()) {
for(el in elements) {
val token = el(env) ?: continue
closeTextToken()
push(env.pos, token)
env.remain = source.substring(env.pos)
continue@loop1
}
if(textToken == null) {
textToken = Token(length = 0)
textTokenStart = env.pos
}
env.remain = source.substring(++ env.pos)
}
closeTextToken()
return sb
}
fun parse(source : String) = Env(source = source).parse()
}
object MisskeyMarkdownDecoder {
@ -505,188 +523,14 @@ object MisskeyMarkdownDecoder {
}
setHighlight()
}
}
private class ParseParams(
val text : String
) {
}
//////////////////////////////////////////////////////
// リンクを作ったりデータを参照したりする要素
private val dEmoji : DecodeEnv.() -> Unit = {
val code = data?.get(0)
if(code?.isNotEmpty() == true) {
appendText(options.decodeEmoji(":$code:"))
}
}
private val dHashtag : DecodeEnv.() -> Unit = {
val linkHelper = linkHelper
val tag = data?.get(0)
if(tag?.isNotEmpty() == true && linkHelper != null) {
appendLink(
"#$tag",
"https://${linkHelper.host}/tags/" + tag.encodePercent()
)
}
}
private val dMention : DecodeEnv.() -> Unit = {
val username = data?.get(0) ?: ""
val host = data?.get(1) ?: ""
val linkHelper = linkHelper
if(linkHelper == null) {
appendText(
when {
host.isEmpty() -> "@$username"
else -> "@$username@$host"
}
)
} else {
val shortAcct = when {
host.isEmpty()
|| host.equals(linkHelper.host, ignoreCase = true) ->
username
else ->
"$username@$host"
}
val userHost = when {
host.isEmpty() -> linkHelper.host
else -> host
}
val userUrl = "https://$userHost/@$username"
var mentions = sb.mentions
if(mentions == null) {
mentions = ArrayList()
sb.mentions = mentions
}
if(mentions.find { it.acct == shortAcct } == null) {
mentions.add(
TootMention(
EntityIdLong(- 1L)
, userUrl
, shortAcct
, username
)
)
}
appendLink(
when {
Pref.bpMentionFullAcct(App1.pref) -> "@$username@$userHost"
else -> "@$shortAcct"
}
, userUrl
)
}
}
//////////////////////////////////////////////////////
// インラインの装飾要素
private val dText : DecodeEnv.() -> Unit = {
appendText(nodeSource)
}
private val dBig : DecodeEnv.() -> Unit = {
appendText(data?.get(0))
setSpan(MisskeyBigSpan(font_bold))
}
private val dBold : DecodeEnv.() -> Unit = {
appendText(data?.get(0))
setSpan(CalligraphyTypefaceSpan(font_bold))
}
private val dCodeInline : DecodeEnv.() -> Unit = {
appendTextCode(data?.get(0))
setSpan(BackgroundColorSpan(0x40808080))
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
}
private val dMotion : DecodeEnv.() -> Unit = {
val code = data?.get(0)
appendText(code)
setSpan(MisskeyMotionSpan(ActMain.timeline_font))
}
private val dUrl : DecodeEnv.() -> Unit = {
val url = data?.get(0)
if(url?.isNotEmpty() == true) {
appendLink(url, url, allowShort = true)
}
}
private val dLink : DecodeEnv.() -> Unit = {
val title = data?.get(0) ?: "?"
val url = data?.get(1)
// val silent = data?.get(2)
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
if(url?.isNotEmpty() == true) {
appendLink(title, url)
}
}
//////////////////////////////////////////////////////
// ブロック要素
private val dCodeBlock : DecodeEnv.() -> Unit = {
closePreviousBlock()
appendTextCode(trimBlock(data?.get(0)))
setSpan(BackgroundColorSpan(0x40808080))
setSpan(RelativeSizeSpan(0.7f))
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
appendText("\n")
}
private val dQuote : DecodeEnv.() -> Unit = {
closePreviousBlock()
appendText(trimBlock(data?.get(0)))
setSpan(BackgroundColorSpan(0x20808080))
setSpan(CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC)))
appendText("\n")
}
private val dTitle : DecodeEnv.() -> Unit = {
closePreviousBlock()
appendText(trimBlock(data?.get(0)))
setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER))
setSpan(BackgroundColorSpan(0x20808080))
setSpan(RelativeSizeSpan(1.5f))
appendText("\n")
}
private val dSearch : DecodeEnv.() -> Unit = {
val text = data?.get(0)
closePreviousBlock()
val kw_start = sb.length // キーワードの開始位置
appendText(text)
appendText(" ")
start = sb.length // 検索リンクの開始位置
appendLink(
context.getString(R.string.search),
"https://www.google.co.jp/search?q=" + (text
?: "Subway Tooter").encodePercent()
)
sb.setSpan(
RelativeSizeSpan(1.2f),
kw_start,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
appendText("\n")
}
//////////////////////////////////////////////////////
private class ParseParams(val text : String) {
var remain : String = ""
var previous : String = ""
var pos : Int = 0
set(value) {
field = value
@ -698,14 +542,14 @@ object MisskeyMarkdownDecoder {
private class Node(
var start : Int, // ソース文字列中の開始位置
var length : Int, // ソース文字列中の長さ
var decoder : DecodeEnv.() -> Unit, // ノード種別
var data : ArrayList<String?>? // パラメータ
var data : ArrayList<String?>?, // パラメータ
var decoder : DecodeEnv.() -> Unit // ノード種別
)
// generate lambda with captured parameter.
private fun simpleParser(
decoder : DecodeEnv.() -> Unit,
pattern : Pattern
, decoder : DecodeEnv.() -> Unit
) : (ParseParams) -> Node? = { env : ParseParams ->
val matcher = pattern.matcher(env.remain)
when {
@ -713,8 +557,8 @@ object MisskeyMarkdownDecoder {
else -> Node(
env.pos,
matcher.end(),
decoder,
arrayListOf(matcher.group(1))
arrayListOf(matcher.group(1)),
decoder
)
}
}
@ -729,33 +573,46 @@ object MisskeyMarkdownDecoder {
addParser(
"\""
, simpleParser(
dQuote,
Pattern.compile("""^"([\s\S]+?)\n"""")
)
, simpleParser(Pattern.compile("""^"([\s\S]+?)\n"""")) {
closePreviousBlock()
appendText(trimBlock(data?.get(0)))
setSpan(BackgroundColorSpan(0x20808080))
setSpan(CalligraphyTypefaceSpan(Typeface.defaultFromStyle(Typeface.ITALIC)))
appendText("\n")
}
)
addParser(
":"
, simpleParser(
dEmoji,
Pattern.compile("""^:([a-zA-Z0-9+-_]+):""")
)
) {
val code = data?.get(0)
if(code?.isNotEmpty() == true) {
appendText(options.decodeEmoji(":$code:"))
}
}
)
val dMotion : DecodeEnv.() -> Unit = {
val code = data?.get(0)
appendText(code)
setSpan(MisskeyMotionSpan(ActMain.timeline_font))
}
addParser(
"("
, simpleParser(
dMotion,
Pattern.compile("""^\Q(((\E(.+?)\Q)))\E""")
, dMotion
)
)
addParser(
"<"
, simpleParser(
dMotion,
Pattern.compile("""^<motion>(.+?)</motion>""")
, dMotion
)
)
@ -764,21 +621,29 @@ object MisskeyMarkdownDecoder {
// 処理順序に意味があるので入れ替えないこと
// 記号列が長い順
, simpleParser(
dBig,
Pattern.compile("""^\Q***\E(.+?)\Q***\E""")
)
) {
appendText(data?.get(0))
setSpan(MisskeyBigSpan(font_bold))
}
, simpleParser(
dBold,
Pattern.compile("""^\Q**\E(.+?)\Q**\E""")
)
) {
appendText(data?.get(0))
setSpan(CalligraphyTypefaceSpan(font_bold))
}
)
addParser(
"h"
, simpleParser(
dUrl,
Pattern.compile("""^(https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+)""")
)
) {
val url = data?.get(0)
if(url?.isNotEmpty() == true) {
appendLink(url, url, allowShort = true)
}
}
)
// 検索だけはボタン開始位置からバックトラックした方が効率的
@ -811,20 +676,48 @@ object MisskeyMarkdownDecoder {
else -> Node(
env.pos - (keyword.length + 1)
, buttonLength + (keyword.length + 1)
, dSearch
, arrayListOf(keyword)
)
) {
val text = data?.get(0)
closePreviousBlock()
val kw_start = sb.length // キーワードの開始位置
appendText(text)
appendText(" ")
start = sb.length // 検索リンクの開始位置
appendLink(
context.getString(R.string.search),
"https://www.google.co.jp/search?q=" + (text
?: "Subway Tooter").encodePercent()
)
sb.setSpan(
RelativeSizeSpan(1.2f),
kw_start,
sb.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
appendText("\n")
}
}
}
}
}
val titleParser = simpleParser(dTitle, Pattern.compile("""^[【\[](.+?)[】\]]\n"""))
val titleParser = simpleParser(
Pattern.compile("""^[【\[](.+?)[】\]]\n""")
) {
closePreviousBlock()
appendText(trimBlock(data?.get(0)))
setSpan(AlignmentSpan.Standard(Layout.Alignment.ALIGN_CENTER))
setSpan(BackgroundColorSpan(0x20808080))
setSpan(RelativeSizeSpan(1.5f))
appendText("\n")
}
val reLink = Pattern.compile(
"""^\??\[([^\[\]]+?)]\((https?://[\w/:%#@${'$'}&?!()\[\]~.=+\-]+?)\)"""
)
val linkParser = { env : ParseParams ->
val matcher = reLink.matcher(env.remain)
when {
@ -832,13 +725,20 @@ object MisskeyMarkdownDecoder {
else -> Node(
env.pos
, matcher.end()
, dLink
, arrayListOf(
matcher.group(1) // title
, matcher.group(2) // url
, env.remain[0].toString() // silent なら "?" になる
)
)
) {
val title = data?.get(0) ?: "?"
val url = data?.get(1)
// val silent = data?.get(2)
// silentはプレビュー表示を抑制するが、Subwayにはもともとないので関係なかった
if(url?.isNotEmpty() == true) {
appendLink(title, url)
}
}
}
}
@ -859,9 +759,62 @@ object MisskeyMarkdownDecoder {
else -> Node(
env.pos
, matcher.end()
, dMention
, arrayListOf(matcher.group(1), matcher.group(2)) // username, host
)
) {
val username = data?.get(0) ?: ""
val host = data?.get(1) ?: ""
val linkHelper = linkHelper
if(linkHelper == null) {
appendText(
when {
host.isEmpty() -> "@$username"
else -> "@$username@$host"
}
)
} else {
val shortAcct = when {
host.isEmpty()
|| host.equals(linkHelper.host, ignoreCase = true) ->
username
else ->
"$username@$host"
}
val userHost = when {
host.isEmpty() -> linkHelper.host
else -> host
}
val userUrl = "https://$userHost/@$username"
var mentions = sb.mentions
if(mentions == null) {
mentions = ArrayList()
sb.mentions = mentions
}
if(mentions.find { it.acct == shortAcct } == null) {
mentions.add(
TootMention(
EntityIdLong(- 1L)
, userUrl
, shortAcct
, username
)
)
}
appendLink(
when {
Pref.bpMentionFullAcct(App1.pref) -> "@$username@$userHost"
else -> "@$shortAcct"
}
, userUrl
)
}
}
}
})
@ -880,10 +833,18 @@ object MisskeyMarkdownDecoder {
else -> Node(
env.pos
, matcher.end()
, dHashtag
, arrayListOf(matcher.group(1))
// 先頭の#を含まないハッシュタグ
)
, arrayListOf(matcher.group(1)) // 先頭の#を含まない
) {
val linkHelper = linkHelper
val tag = data?.get(0)
if(tag?.isNotEmpty() == true && linkHelper != null) {
appendLink(
"#$tag",
"https://${linkHelper.host}/tags/" + tag.encodePercent()
)
}
}
}
}
}
@ -892,14 +853,24 @@ object MisskeyMarkdownDecoder {
addParser(
"`"
, simpleParser(
dCodeBlock,
Pattern.compile("""^```(.+?)```""", Pattern.DOTALL)
)
) {
closePreviousBlock()
appendTextCode(trimBlock(data?.get(0)))
setSpan(BackgroundColorSpan(0x40808080))
setSpan(RelativeSizeSpan(0.7f))
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
appendText("\n")
}
, simpleParser(
dCodeInline,
Pattern.compile("""^`([^`´\x0d\x0a]+)`""")
// インラインコードは内部にとある文字を含むと認識されない。理由は顔文字と衝突するからだとか
)
Pattern.compile("""^`([^`´\x0d\x0a]+)`""")
) {
appendTextCode(data?.get(0))
setSpan(BackgroundColorSpan(0x40808080))
setSpan(CalligraphyTypefaceSpan(Typeface.MONOSPACE))
}
)
}
@ -912,15 +883,7 @@ object MisskeyMarkdownDecoder {
.replace(reEndEmptyLines, "")
}
// 配列中の要素をラムダ式で変換して、戻り値が非nullならそこで処理を打ち切る
private inline fun <T, V> Array<out T>.firstNonNull(predicate : (T) -> V?) : V? {
for(element in this) return predicate(element) ?: continue
return null
}
private fun parse(source : String?) : ArrayList<Node> {
val result = ArrayList<Node>()
private fun parse(source : String?, callback : (Node) -> Unit) {
if(source != null) {
val env = ParseParams(source)
val end = source.length
@ -930,7 +893,11 @@ object MisskeyMarkdownDecoder {
// 直前のードの終了位置から次のードの開始位置の手前までをresultに追加する
fun closeText(endText : Int) {
val length = endText - lastEnd
if(length > 0) result.add(Node(lastEnd, length, dText, null))
if(length > 0) callback(
Node(lastEnd, length, null) {
appendText(nodeSource)
}
)
}
while(pos < end) {
@ -941,19 +908,17 @@ object MisskeyMarkdownDecoder {
}
env.pos = pos
val node = lastParsers.firstNonNull { it(env) }
if( node == null) {
if(node == null) {
++ pos
continue
}
closeText(node.start)
result.add(node)
callback(node)
lastEnd = node.start + node.length
pos = lastEnd
}
closeText(pos)
}
return result
}
fun decodeMarkdown(options : DecodeOptions, src : String?) =
@ -961,14 +926,12 @@ object MisskeyMarkdownDecoder {
try {
val env = DecodeEnv(options, this)
if(src != null) {
for(node in parse(src)) {
env.nodeSource = src.substring(node.start, node.start + node.length)
env.start = length
env.data = node.data
val decoder = node.decoder
env.decoder()
}
if(src != null) parse(src) { node ->
env.nodeSource = src.substring(node.start, node.start + node.length)
env.start = length
env.data = node.data
val decoder = node.decoder
env.decoder()
}
// 末尾の空白を取り除く

View File

@ -967,6 +967,115 @@
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/background_color_unlisted"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnBackgroundColorUnlistedEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnBackgroundColorUnlistedReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/background_color_follower"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnBackgroundColorFollowerEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnBackgroundColorFollowerReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/background_color_direct_with_user"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnBackgroundColorDirectWithUserEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnBackgroundColorDirectWithUserReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/background_color_direct_no_user"
/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnBackgroundColorDirectNoUserEdit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/edit"
android:textAllCaps="false"
/>
<Button
android:id="@+id/btnBackgroundColorDirectNoUserReset"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/reset"
android:textAllCaps="false"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView

View File

@ -772,6 +772,10 @@
<string name="reply_to">Reply to %1$s</string>
<string name="deleted_at">deleted at %1$s</string>
<string name="already_reactioned">already reactioned.</string>
<string name="background_color_unlisted">Toot background color of \'Unlisted\' visibility</string>
<string name="background_color_follower">Toot background color of \'follower\' visibility</string>
<string name="background_color_direct_with_user">Toot background color of \'Direct to users\' visibility</string>
<string name="background_color_direct_no_user">Toot background color of \'Direct only me\' visibility</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->

View File

@ -1049,5 +1049,9 @@
<string name="reply_to">%1$sへの返信</string>
<string name="deleted_at">%1$sに削除されました</string>
<string name="already_reactioned">リアクション済みです</string>
<string name="background_color_unlisted">トゥート背景色 \'未収載\'</string>
<string name="background_color_follower">トゥート背景色 \'フォロワーのみ\'</string>
<string name="background_color_direct_with_user">トゥート背景色 \'ダイレクト(対象ユーザあり)\'</string>
<string name="background_color_direct_no_user">トゥート背景色 \'ダイレクト(自分のみ)\'</string>
</resources>

View File

@ -757,5 +757,9 @@
<string name="reply_to">Reply to %1$s</string>
<string name="deleted_at">deleted at %1$s</string>
<string name="already_reactioned">already reactioned.</string>
<string name="background_color_unlisted">Toot background color of \'Unlisted\' visibility</string>
<string name="background_color_follower">Toot background color of \'follower\' visibility</string>
<string name="background_color_direct_with_user">Toot background color of \'Direct to users\' visibility</string>
<string name="background_color_direct_no_user">Toot background color of \'Direct only me\' visibility</string>
</resources>

View File

@ -17,6 +17,9 @@ android {
dependencies {
implementation "com.android.support:appcompat-v7:$asl_version"
implementation 'com.google.android:flexbox:0.3.2'
}
//apply plugin: 'com.getkeepsafe.dexcount'

View File

@ -4,7 +4,9 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
tools:ignore="RtlHardcoded"
>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
@ -18,16 +20,20 @@
style="@style/cpv_ColorPickerViewStyle"
android:padding="16dp"/>
<LinearLayout
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
tools:ignore="RtlHardcoded"
app:flexWrap="wrap"
app:justifyContent="flex_start"
>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_old"
android:layout_width="@dimen/cpv_dialog_preview_width"
@ -49,11 +55,7 @@
android:layout_width="@dimen/cpv_dialog_preview_width"
android:layout_height="@dimen/cpv_dialog_preview_height"
app:cpv_colorShape="square"/>
<Space
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_weight="1"/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
@ -63,18 +65,20 @@
android:focusableInTouchMode="true"
android:gravity="right"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#"
android:typeface="monospace"
tools:ignore="HardcodedText"/>
tools:ignore="HardcodedText"
android:labelFor="@+id/cpv_hex"
/>
<EditText
android:id="@+id/cpv_hex"
android:layout_width="110dp"
android:layout_width="110sp"
android:layout_height="wrap_content"
android:digits="0123456789ABCDEFabcdef"
android:focusable="true"
@ -87,7 +91,7 @@
</LinearLayout>
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
</ScrollView>

View File

@ -4,87 +4,103 @@
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<com.jrummyapps.android.colorpicker.ColorPickerView
android:id="@id/cpv_color_picker_view"
style="@style/cpv_ColorPickerViewStyle"
android:padding="16dp"/>
android:layout_height="wrap_content"
>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
tools:ignore="RtlHardcoded"
>
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_old"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
app:cpv_colorShape="square"
<com.jrummyapps.android.colorpicker.ColorPickerView
android:id="@id/cpv_color_picker_view"
style="@style/cpv_ColorPickerViewStyle"
android:padding="16dp"
/>
<ImageView
android:id="@+id/cpv_arrow_right"
android:layout_width="wrap_content"
<com.google.android.flexbox.FlexboxLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:src="@drawable/cpv_ic_arrow_right_black_24dp"
tools:ignore="ContentDescription"/>
android:paddingBottom="16dp"
android:paddingLeft="16dp"
android:paddingRight="16dp"
app:flexWrap="wrap"
app:justifyContent="flex_start"
tools:ignore="RtlHardcoded"
>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_new"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="match_parent"
app:cpv_colorShape="square"/>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="8dp"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:gravity="right"
android:orientation="horizontal"
tools:ignore="RtlHardcoded">
<TextView
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#"
android:typeface="monospace"
tools:ignore="HardcodedText"/>
>
<EditText
android:id="@+id/cpv_hex"
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_old"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:cpv_colorShape="square"
/>
<ImageView
android:id="@+id/cpv_arrow_right"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:paddingLeft="3dp"
android:paddingRight="3dp"
android:src="@drawable/cpv_ic_arrow_right_black_24dp"
tools:ignore="ContentDescription"
/>
<com.jrummyapps.android.colorpicker.ColorPanelView
android:id="@id/cpv_color_panel_new"
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
app:cpv_colorShape="square"
/>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:digits="0123456789ABCDEFabcdef"
android:focusable="true"
android:imeOptions="actionGo"
android:inputType="textNoSuggestions"
android:maxLength="8"
android:maxLines="1"
android:minWidth="110dp"
android:typeface="monospace"
tools:text="88888888"/>
android:layout_marginLeft="8dp"
android:descendantFocusability="beforeDescendants"
android:focusableInTouchMode="true"
android:gravity="right"
android:orientation="horizontal"
tools:ignore="RtlHardcoded"
>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="#"
android:typeface="monospace"
tools:ignore="HardcodedText"
/>
<EditText
android:id="@+id/cpv_hex"
android:layout_width="110sp"
android:layout_height="wrap_content"
android:digits="0123456789ABCDEFabcdef"
android:focusable="true"
android:imeOptions="actionGo"
android:inputType="textNoSuggestions"
android:maxLength="8"
android:maxLines="1"
android:minWidth="110dp"
android:typeface="monospace"
tools:text="88888888"
/>
</LinearLayout>
</com.google.android.flexbox.FlexboxLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</ScrollView>