「強調表示する単語/編集/読み上げを有効にする」を追加。どのキーワードに反応したのかカラムを辿らなくても分かる。

This commit is contained in:
tateisu 2019-11-15 15:13:59 +09:00
parent 096a8770db
commit 73360e2269
18 changed files with 303 additions and 161 deletions

View File

@ -49,6 +49,8 @@ class ActHighlightWordEdit
private lateinit var tvName : TextView
private lateinit var swSound : Switch
private lateinit var swSpeech : Switch
private var bBusy = false
@ -60,7 +62,6 @@ class ActHighlightWordEdit
} catch(ex : JSONException) {
throw RuntimeException(ex)
}
}
override fun onBackPressed() {
@ -110,12 +111,19 @@ class ActHighlightWordEdit
swSound = findViewById(R.id.swSound)
swSound.setOnCheckedChangeListener(this)
findViewById<View>(R.id.btnTextColorEdit).setOnClickListener(this)
findViewById<View>(R.id.btnTextColorReset).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorEdit).setOnClickListener(this)
findViewById<View>(R.id.btnBackgroundColorReset).setOnClickListener(this)
findViewById<View>(R.id.btnNotificationSoundEdit).setOnClickListener(this)
findViewById<View>(R.id.btnNotificationSoundReset).setOnClickListener(this)
swSpeech = findViewById(R.id.swSpeech)
swSpeech.setOnCheckedChangeListener(this)
intArrayOf(
R.id.btnTextColorEdit,
R.id.btnTextColorReset,
R.id.btnBackgroundColorEdit,
R.id.btnBackgroundColorReset,
R.id.btnNotificationSoundEdit,
R.id.btnNotificationSoundReset
).forEach {
findViewById<View>(it)?.setOnClickListener(this)
}
}
private fun showSampleText() {
@ -124,6 +132,8 @@ class ActHighlightWordEdit
swSound.isChecked = item.sound_type != HighlightWord.SOUND_TYPE_NONE
swSpeech.isChecked = item.speech != 0
tvName.text = item.name
tvName.setBackgroundColor(item.color_bg) // may 0
tvName.textColor = item.color_fg.notZero()
@ -169,12 +179,21 @@ class ActHighlightWordEdit
override fun onCheckedChanged(buttonView : CompoundButton, isChecked : Boolean) {
if(bBusy) return
if(! isChecked) {
item.sound_type = HighlightWord.SOUND_TYPE_NONE
} else {
item.sound_type =
if(item.sound_uri?.isEmpty() != false) HighlightWord.SOUND_TYPE_DEFAULT else HighlightWord.SOUND_TYPE_CUSTOM
when(buttonView.id){
R.id.swSound ->{
if(! isChecked) {
item.sound_type = HighlightWord.SOUND_TYPE_NONE
} else {
item.sound_type =
if(item.sound_uri?.isEmpty() != false) HighlightWord.SOUND_TYPE_DEFAULT else HighlightWord.SOUND_TYPE_CUSTOM
}
}
R.id.swSpeech->{
item.speech = when(isChecked){
false->0
else->1
}
}
}
}

View File

@ -11,6 +11,7 @@ import androidx.appcompat.app.AppCompatActivity
import androidx.recyclerview.widget.LinearLayoutManager
import android.view.View
import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.TextView
import com.woxthebox.draglistview.DragItem
import com.woxthebox.draglistview.DragItemAdapter
@ -95,7 +96,7 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
if(swipedDirection == ListSwipeItem.SwipeDirection.LEFT) {
val o = item.tag
if(o is HighlightWord) {
o.delete()
o.delete(this@ActHighlightWordList)
listAdapter.removeItem(listAdapter.getPositionForItem(o))
}
}
@ -135,11 +136,13 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
val tvName : TextView
private val btnSound : View
val ivSpeech: ImageButton
init {
tvName = viewRoot.findViewById(R.id.tvName)
btnSound = viewRoot.findViewById(R.id.btnSound)
ivSpeech = viewRoot.findViewById(R.id.ivSpeech)
// リスト要素のビューが ListSwipeItem だった場合、Swipe操作を制御できる
if(viewRoot is ListSwipeItem) {
@ -163,6 +166,10 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
vg(btnSound, item.sound_type != HighlightWord.SOUND_TYPE_NONE)
btnSound.setOnClickListener(this)
btnSound.tag = item
vg(ivSpeech,item.speech != 0 )
ivSpeech.setOnClickListener(this)
ivSpeech.tag = item
}
// @Override
@ -180,9 +187,15 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
override fun onClick(v : View) {
val o = v.tag
if(o is HighlightWord) {
sound(o)
when(v.id){
R.id.btnSound->{
sound(o)
}
R.id.ivSpeech->{
App1.getAppState(this@ActHighlightWordList).addSpeech(o.name,allowRepeat = true)
}
}
}
}
}
@ -191,10 +204,16 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
DragItem(context, layoutId) {
override fun onBindDragView(clickedView : View, dragView : View) {
dragView.findViewById<TextView>(R.id.tvName).text =
clickedView.findViewById<TextView>(R.id.tvName).text
dragView.findViewById<View>(R.id.btnSound).visibility =
clickedView.findViewById<View>(R.id.btnSound).visibility
dragView.findViewById<View>(R.id.ivSpeech).visibility=
clickedView.findViewById<View>(R.id.ivSpeech).visibility
dragView.findViewById<View>(R.id.item_layout).setBackgroundColor(
getAttributeColor(
this@ActHighlightWordList,
@ -238,7 +257,7 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
var item = HighlightWord.load(text)
if(item == null) {
item = HighlightWord(text)
item.save()
item.save(this@ActHighlightWordList)
loadData()
}
edit(item)
@ -260,7 +279,7 @@ class ActHighlightWordList : AppCompatActivity(), View.OnClickListener {
try {
val sv = data.getStringExtra(ActHighlightWordEdit.EXTRA_ITEM) ?: return
val item = HighlightWord(sv.toJsonObject())
item.save()
item.save(this@ActHighlightWordList)
loadData()
} catch(ex : Throwable) {
throw RuntimeException("can't load data", ex)

View File

@ -127,7 +127,9 @@ class App1 : Application() {
// 2019/10/22 39 => 40 NotificationTracking テーブルに項目追加。
// 2019/10/22 40 => 41 NotificationCache テーブルに項目追加。
// 2019/10/23 41=> 42 SavedAccount テーブルに項目追加。
internal const val DB_VERSION = 42
// 2019/11/15 42=> 43 HighlightWord テーブルに項目追加。
internal const val DB_VERSION = 43
private val tableList = arrayOf(
LogData,

View File

@ -142,7 +142,7 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
this.handler = Handler()
this.density = context.resources.displayMetrics.density
this.stream_reader = StreamReader(context, handler, pref)
this.networkTracker = NetworkStateTracker(context){
this.networkTracker = NetworkStateTracker(context) {
App1.custom_emoji_cache.onNetworkChanged()
App1.custom_emoji_lister.onNetworkChanged()
}
@ -153,16 +153,7 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
// TextToSpeech
private val isTextToSpeechRequired : Boolean
get() {
var b = false
for(c in column_list) {
if(c.enable_speech) {
b = true
break
}
}
return b
}
get() = column_list.any { it.enable_speech } || HighlightWord.hasTextToSpeechHighlightWord()
private val tts_receiver = object : BroadcastReceiver() {
override fun onReceive(context : Context, intent : Intent?) {
@ -345,8 +336,6 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
map_busy_bookmark.remove(key)
}
fun isBusyBoost(account : SavedAccount, status : TootStatus) : Boolean {
val key = account.acct + ":" + status.busyKey
return map_busy_boost.contains(key)
@ -363,7 +352,7 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
}
@SuppressLint("StaticFieldLeak")
private fun enableSpeech() {
fun enableSpeech() {
this.willSpeechEnabled = isTextToSpeechRequired
if(willSpeechEnabled && tts == null && tts_status == TTS_STATUS_NONE) {
@ -526,19 +515,21 @@ class AppState(internal val context : Context, internal val pref : SharedPrefere
addSpeech(sb.toString())
}
private fun addSpeech(text : String) {
internal fun addSpeech(text : String,allowRepeat:Boolean = false) {
if(tts == null) return
val sv = reSpaces.matcher(text).replaceAll(" ").trim { it <= ' ' }
if(sv.isEmpty()) return
for(check in duplication_check) {
if(check == sv) return
}
duplication_check.addLast(sv)
if(duplication_check.size >= 60) {
duplication_check.removeFirst()
if(!allowRepeat) {
for(check in duplication_check) {
if(check == sv) return
}
duplication_check.addLast(sv)
if(duplication_check.size >= 60) {
duplication_check.removeFirst()
}
}
tts_queue.add(sv)

View File

@ -1411,7 +1411,7 @@ class Column(
if(! (with_attachment || with_highlight)) return false
val matchMedia = with_attachment && status.reblog?.hasMedia() ?: status.hasMedia()
val matchHighlight = with_highlight && status.reblog?.hasHighlight ?: status.hasHighlight
val matchHighlight = with_highlight && null != (status.reblog?.highlightAny ?: status.highlightAny)
// どれかの条件を満たすならフィルタしない(false)、どれも満たさないならフィルタする(true)
return ! (matchMedia || matchHighlight)
@ -2690,16 +2690,18 @@ class Column(
val added = list_new.size // may 0
loop@ for(o in list_new) {
when(o) {
is TootStatus -> {
val highlight_sound = o.highlight_sound
if(highlight_sound != null) {
App1.sound(highlight_sound)
break@loop
var doneSound = false
for(o in list_new) {
if( o is TootStatus ){
o.highlightSound?.let{
if(!doneSound){
doneSound = true
App1.sound(it)
}
}
o.highlightSpeech?.let{
App1.getAppState(context).addSpeech(it.name,allowRepeat = true)
}
}
}

View File

@ -134,12 +134,17 @@ class ColumnTask_Refresh(
column.list_data.removeAt(0)
}
var doneSound = false
for(o in list_new) {
if(o is TootStatus) {
val highlight_sound = o.highlight_sound
if(highlight_sound != null) {
App1.sound(highlight_sound)
break
if( o is TootStatus ){
o.highlightSound?.let{
if(!doneSound){
doneSound = true
App1.sound(it)
}
}
o.highlightSpeech?.let{
App1.getAppState(context).addSpeech(it.name,allowRepeat = true)
}
}
}

View File

@ -25,36 +25,36 @@ class DownloadReceiver : BroadcastReceiver() {
try {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, 0L)
val query = DownloadManager.Query().setFilterById(id)
downloadManager.query(query)?.use { cursor ->
if(! cursor.moveToFirst()) {
log.e("cursor.moveToFirst() failed.")
} else {
val title = cursor.getStringOrNull(DownloadManager.COLUMN_TITLE)
val status = cursor.getIntOrNull(DownloadManager.COLUMN_STATUS)
showToast(
context,
false,
if(status == DownloadManager.STATUS_SUCCESSFUL) {
context.getString(R.string.download_complete, title)
} else {
context.getString(R.string.download_failed, title)
}
)
/*
ダウンロード完了通知がシステムからのものと重複することがある
- (Aubee elm. Android 5.1) don't shows toast.
- (Samsung Galaxy S8+ Android 7.0) don't show toast.
- (Kyocera AndroidOne Android 8.0 S2) don't show toast.
- (LGE LGL24 Android 5.0.2) SHOWS toast.
- (LGE LGV32 Android 6.0) SHOWS toast.
maybe it depends on customization by device maker. not depends on OS version.
重複を回避する方法はなさそうだ
*/
return
}
val title = cursor.getStringOrNull(DownloadManager.COLUMN_TITLE)
val status = cursor.getIntOrNull(DownloadManager.COLUMN_STATUS)
showToast(
context,
false,
if(status == DownloadManager.STATUS_SUCCESSFUL) {
context.getString(R.string.download_complete, title)
} else {
context.getString(R.string.download_failed, title)
}
)
/*
ダウンロード完了通知がシステムからのものと重複することがある
- (Aubee elm. Android 5.1) don't shows toast.
- (Samsung Galaxy S8+ Android 7.0) don't show toast.
- (Kyocera AndroidOne Android 8.0 S2) don't show toast.
- (LGE LGL24 Android 5.0.2) SHOWS toast.
- (LGE LGV32 Android 6.0) SHOWS toast.
maybe it depends on customization by device maker. not depends on OS version.
重複を回避する方法はなさそうだ
*/
}
} catch(ex : Throwable) {
log.e(ex, "downloadManager.query() failed.")

View File

@ -181,9 +181,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
// 会話の流れビューで後から追加する
var card : TootCard? = null
var highlight_sound : HighlightWord? = null
var hasHighlight : Boolean = false
var highlightSound: HighlightWord? = null
var highlightSpeech: HighlightWord? = null
var highlightAny: HighlightWord? = null
val time_created_at : Long
@ -295,10 +295,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
)
this.decoded_content = options.decodeHTML(content)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
if(this.highlightSound==null) this.highlightSound = options.highlightSound
if(this.highlightSpeech==null) this.highlightSpeech = options.highlightSpeech
if(this.highlightAny==null) this.highlightAny = options.highlightAny
// Markdownのデコード結果からmentionsを読むのだった
val mentions1 =
@ -325,10 +324,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
)
this.decoded_spoiler_text = options.decodeHTML(spoiler_text)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
if(this.highlightSound==null) this.highlightSound = options.highlightSound
if(this.highlightSpeech==null) this.highlightSpeech = options.highlightSpeech
if(this.highlightAny==null) this.highlightAny = options.highlightAny
val mentions2 =
(decoded_spoiler_text as? MisskeyMarkdownDecoder.SpannableStringBuilderEx)?.mentions
@ -494,10 +492,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
)
this.decoded_content = options.decodeHTML(content)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
if(this.highlightSound==null) this.highlightSound = options.highlightSound
if(this.highlightSpeech==null) this.highlightSpeech = options.highlightSpeech
if(this.highlightAny==null) this.highlightAny = options.highlightAny
var sv = (src.parseString("spoiler_text") ?: "").cleanCW()
this.spoiler_text = when {
@ -517,10 +514,9 @@ class TootStatus(parser : TootParser, src : JSONObject) : TimelineItem() {
this.decoded_spoiler_text = options.decodeEmoji(spoiler_text)
this.hasHighlight = this.hasHighlight || options.hasHighlight
if(options.highlight_sound != null && this.highlight_sound == null) {
this.highlight_sound = options.highlight_sound
}
if(this.highlightSound==null) this.highlightSound = options.highlightSound
if(this.highlightSpeech==null) this.highlightSpeech = options.highlightSpeech
if(this.highlightAny==null) this.highlightAny = options.highlightAny
this.enquete = try {
sv = src.parseString("enquete") ?: ""

View File

@ -1,17 +1,20 @@
package jp.juggler.subwaytooter.table
import android.content.ContentValues
import android.content.Context
import android.database.Cursor
import android.database.sqlite.SQLiteDatabase
import android.provider.BaseColumns
import org.json.JSONObject
import jp.juggler.subwaytooter.App1
import jp.juggler.util.*
import java.util.concurrent.atomic.AtomicReference
class HighlightWord {
companion object :TableCompanion{
companion object : TableCompanion {
private val log = LogCategory("HighlightWord")
@ -20,34 +23,37 @@ class HighlightWord {
const val SOUND_TYPE_CUSTOM = 2
const val table = "highlight_word"
const val COL_ID = "_id"
const val COL_ID = BaseColumns._ID
const val COL_NAME = "name"
private const val COL_TIME_SAVE = "time_save"
private const val COL_COLOR_BG = "color_bg"
private const val COL_COLOR_FG = "color_fg"
private const val COL_SOUND_TYPE = "sound_type"
private const val COL_SOUND_URI = "sound_uri"
private const val COL_SPEECH = "speech"
private const val selection_name = COL_NAME + "=?"
private const val selection_id = COL_ID + "=?"
private const val selection_name = "$COL_NAME=?"
private const val selection_speech = "$COL_SPEECH<>0"
private const val selection_id = "$COL_ID=?"
private val columns_name = arrayOf(COL_NAME)
override fun onDBCreate(db : SQLiteDatabase) {
log.d("onDBCreate!")
db.execSQL(
"create table if not exists " + table
+ "(_id INTEGER PRIMARY KEY"
+ ",name text not null"
+ ",time_save integer not null"
+ ",color_bg integer not null default 0"
+ ",color_fg integer not null default 0"
+ ",sound_type integer not null default 1"
+ ",sound_uri text default null"
+ ")"
"""create table if not exists $table
($COL_ID INTEGER PRIMARY KEY
,$COL_NAME text not null
,$COL_TIME_SAVE integer not null
,$COL_COLOR_BG integer not null default 0
,$COL_COLOR_FG integer not null default 0
,$COL_SOUND_TYPE integer not null default 1
,$COL_SOUND_URI text default null
,$COL_SPEECH integer default 0
)"""
)
db.execSQL(
"create unique index if not exists " + table + "_name on " + table + "(name)"
"create unique index if not exists ${table}_name on $table(name)"
)
}
@ -55,6 +61,13 @@ class HighlightWord {
if(oldVersion < 21 && newVersion >= 21) {
onDBCreate(db)
}
if(oldVersion < 43 && newVersion >= 43) {
try {
db.execSQL("alter table $table add column $COL_SPEECH integer default 0")
} catch(ex : Throwable) {
log.trace(ex)
}
}
}
fun load(name : String) : HighlightWord? {
@ -73,7 +86,7 @@ class HighlightWord {
}
fun createCursor() : Cursor {
return App1.database.query(table, null, null, null, null, null, COL_NAME + " asc")
return App1.database.query(table, null, null, null, null, null, "$COL_NAME asc")
}
val nameSet : WordTrieTree?
@ -95,14 +108,46 @@ class HighlightWord {
return if(dst.isEmpty) null else dst
}
private val hasTextToSpeechHighlightWordCache = AtomicReference<Boolean>(null)
fun hasTextToSpeechHighlightWord() : Boolean {
synchronized(this) {
var cached = hasTextToSpeechHighlightWordCache.get()
if(cached == null) {
cached = false
try {
App1.database.query(
table,
columns_name,
selection_speech,
null,
null,
null,
null
)
.use { cursor ->
while(cursor.moveToNext()) {
cached = true
}
}
} catch(ex : Throwable) {
log.trace(ex)
}
hasTextToSpeechHighlightWordCache.set(cached)
}
return cached
}
}
}
var id = - 1L
var name : String
var color_bg : Int = 0
var color_fg : Int = 0
var sound_type : Int = 0
var color_bg = 0
var color_fg = 0
var sound_type = 0
var sound_uri : String? = null
var speech = 0
fun encodeJson() : JSONObject {
val dst = JSONObject()
@ -111,17 +156,19 @@ class HighlightWord {
dst.put(COL_COLOR_BG, color_bg)
dst.put(COL_COLOR_FG, color_fg)
dst.put(COL_SOUND_TYPE, sound_type)
dst.put(COL_SPEECH, speech)
if(sound_uri != null) dst.put(COL_SOUND_URI, sound_uri)
return dst
}
constructor(src : JSONObject) {
this.id = src.parseLong( COL_ID) ?: -1L
this.id = src.parseLong(COL_ID) ?: - 1L
this.name = src.notEmptyOrThrow(COL_NAME)
this.color_bg = src.optInt(COL_COLOR_BG)
this.color_fg = src.optInt(COL_COLOR_FG)
this.sound_type = src.optInt(COL_SOUND_TYPE)
this.sound_uri = src.parseString( COL_SOUND_URI)
this.sound_uri = src.parseString(COL_SOUND_URI)
this.speech = src.optInt(COL_SPEECH)
}
constructor(name : String) {
@ -137,9 +184,10 @@ class HighlightWord {
this.color_fg = cursor.getInt(COL_COLOR_FG)
this.sound_type = cursor.getInt(COL_SOUND_TYPE)
this.sound_uri = cursor.getStringOrNull(COL_SOUND_URI)
this.speech = cursor.getInt(COL_SPEECH)
}
fun save() {
fun save(context : Context) {
if(name.isEmpty()) throw RuntimeException("HighlightWord.save(): name is empty")
try {
@ -149,12 +197,15 @@ class HighlightWord {
cv.put(COL_COLOR_BG, color_bg)
cv.put(COL_COLOR_FG, color_fg)
cv.put(COL_SOUND_TYPE, sound_type)
val sound_uri = this.sound_uri
if(sound_uri?.isEmpty() != false) {
cv.putNull(COL_SOUND_URI)
} else {
cv.put(COL_SOUND_URI, sound_uri)
}
cv.put(COL_SPEECH, speech)
if(id == - 1L) {
id = App1.database.replace(table, null, cv)
} else {
@ -164,14 +215,22 @@ class HighlightWord {
log.e(ex, "save failed.")
}
synchronized(Companion) {
hasTextToSpeechHighlightWordCache.set(null)
}
App1.getAppState(context).enableSpeech()
}
fun delete() {
fun delete(context : Context) {
try {
App1.database.delete(table, selection_id, arrayOf(id.toString()))
} catch(ex : Throwable) {
log.e(ex, "delete failed.")
}
synchronized(Companion) {
hasTextToSpeechHighlightWordCache.set(null)
}
App1.getAppState(context).enableSpeech()
}
}

View File

@ -36,11 +36,10 @@ class DecodeOptions(
return false
}
// OUTPUT: true if found highlight
var hasHighlight : Boolean = false
// OUTPUT: found highlight with sound
var highlight_sound : HighlightWord? = null
// OUTPUT
var highlightSound: HighlightWord? = null
var highlightSpeech: HighlightWord? = null
var highlightAny: HighlightWord? = null
////////////////////////
// decoder

View File

@ -107,19 +107,23 @@ object EmojiDecoder {
private fun applyHighlight(start : Int, end : Int) {
val list = options.highlightTrie?.matchList(sb, start, end) ?: return
for(range in list) {
val word = HighlightWord.load(range.word)
if(word != null) {
options.hasHighlight = true
sb.setSpan(
HighlightSpan(word.color_fg, word.color_bg),
range.start,
range.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
options.highlight_sound = word
}
val word = HighlightWord.load(range.word) ?: continue
sb.setSpan(
HighlightSpan(word.color_fg, word.color_bg),
range.start,
range.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
if(options.highlightSound == null) options.highlightSound = word
}
if( word.speech != 0 ) {
if(options.highlightSpeech == null) options.highlightSpeech = word
}
if(options.highlightAny == null) options.highlightAny = word
}
}

View File

@ -408,19 +408,23 @@ object HTMLDecoder {
val list = options.highlightTrie?.matchList(sb, start, end)
if(list != null) {
for(range in list) {
val word = HighlightWord.load(range.word)
if(word != null) {
options.hasHighlight = true
sb.setSpan(
HighlightSpan(word.color_fg, word.color_bg),
range.start,
range.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
options.highlight_sound = word
}
val word = HighlightWord.load(range.word) ?: continue
sb.setSpan(
HighlightSpan(word.color_fg, word.color_bg),
range.start,
range.end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
)
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
if(options.highlightSound == null) options.highlightSound = word
}
if( word.speech != 0 ) {
if(options.highlightSpeech == null) options.highlightSpeech = word
}
if(options.highlightAny == null) options.highlightAny = word
}
}
}

View File

@ -732,18 +732,22 @@ object MisskeyMarkdownDecoder {
val list = options.highlightTrie?.matchList(sb, start, end)
if(list != null) {
for(range in list) {
val word = HighlightWord.load(range.word)
if(word != null) {
options.hasHighlight = true
spanList.addLast(
range.start,
range.end,
HighlightSpan(word.color_fg, word.color_bg)
)
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
options.highlight_sound = word
}
val word = HighlightWord.load(range.word) ?: continue
spanList.addLast(
range.start,
range.end,
HighlightSpan(word.color_fg, word.color_bg)
)
if(word.sound_type != HighlightWord.SOUND_TYPE_NONE) {
if(options.highlightSound == null) options.highlightSound = word
}
if( word.speech != 0 ) {
if(options.highlightSpeech == null) options.highlightSpeech = word
}
if(options.highlightAny == null) options.highlightAny = word
}
}
}

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M21.99,4c0,-1.1 -0.89,-2 -1.99,-2L4,2c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h14l4,4 -0.01,-18zM18,14L6,14v-2h12v2zM18,11L6,11L6,9h12v2zM18,8L6,8L6,6h12v2z"/>
</vector>

View File

@ -123,6 +123,23 @@
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/enable_speech"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swSpeech"
style="@style/setting_horizontal_stretch"
android:gravity="center"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>

View File

@ -66,6 +66,15 @@
android:gravity="center_vertical|start"
android:paddingStart="12dp"
android:paddingEnd="12dp"
android:layout_marginEnd="4dp"
/>
<ImageButton
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/ivSpeech"
android:src="@drawable/ic_comment"
android:tint="?attr/colorVectorDrawable"
android:layout_marginEnd="4dp"
/>
<ImageButton
android:layout_width="wrap_content"
@ -74,6 +83,7 @@
android:src="@drawable/ic_volume_up"
android:contentDescription="@string/check_sound"
android:tint="?attr/colorVectorDrawable"
android:layout_marginEnd="4dp"
/>
</LinearLayout>

View File

@ -970,5 +970,6 @@
<string name="always_expand_context_menu_sub_items" >コンテキストメニューの副項目を常に展開する</string>
<string name="release_notes_latest">更新履歴 (v%1$s)</string>
<string name="release_notes_updated">更新履歴 (v%1$s→v%2$s)</string>
<string name="test">Test</string>
</resources>

View File

@ -966,4 +966,5 @@
<string name="always_expand_context_menu_sub_items" >Always expand sub-items of context menu</string>
<string name="release_notes_latest">Release notes (v%1$s)</string>
<string name="release_notes_updated">Release notes (v%1$s→v%2$s)</string>
<string name="test">Test</string>
</resources>