- 単語フィルタ編集画面の初期状態でソフトキーボードを表示しない

- 期限を指定したフィルタを後から無期限にする方法がないので、秒数にInt.MAX_VALUE shr 1 を渡す
- 単語フィルタ一覧カラムで上端スワイプするとリロード
- 単語フィルタ一覧の項目表示を少しキレイにした
- 単語フィルタ一覧で不可逆フラグを表示する
- 単語フィルタは単語[A-Za-z0-9_]の区切りを意識したマッチングを行う
- 単語フィルタの作成で適用箇所の初期状態を全てチェック済みに変更
- 単語フィルタの編集画面で保存ボタンをスクロールビューの外側に配置
This commit is contained in:
tateisu 2018-07-09 02:00:47 +09:00
parent 218b832587
commit eccee68092
10 changed files with 196 additions and 127 deletions

View File

@ -190,6 +190,7 @@
<activity
android:name=".ActKeywordFilter"
android:label="@string/keyword_filter_new"
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
/>
<activity

View File

@ -240,13 +240,13 @@ class ActKeywordFilter
// dont change
- 1 -> put("expires_in", "")
// set unlimited
// unlimited
0 -> if(filter_id == - 1L) {
// set blank to dont set expire
// don't specify expires_in for creating
put("expires_in", "")
} else {
// FIXME: currently no way to remove expire from existing filter
put("expires_in", (Int.MAX_VALUE shr 5))
// FIXME: currently there is no way to remove expires from existing filter.
put("expires_in", (Int.MAX_VALUE shr 1))
}
// set seconds

View File

@ -3684,6 +3684,7 @@ class Column(
fun canReloadWhenRefreshTop() : Boolean {
return when(column_type) {
TYPE_KEYWORD_FILTER,
TYPE_SEARCH,
TYPE_SEARCH_MSP,
TYPE_SEARCH_TS,
@ -3990,7 +3991,7 @@ class Column(
private fun encodeFilterTree(filterList : ArrayList<TootFilter>?) : WordTrieTree? {
val column_context = getFilterContext()
if(column_context == 0 || filterList == null) return null
val tree = WordTrieTree()
val tree = WordTrieTree(WordTrieTree.WORD_VALIDATOR)
for(filter in filterList) {
if((filter.context and column_context) != 0) {
tree.add(filter.phrase)

View File

@ -316,7 +316,7 @@ class ColumnViewHolder(
}
private val proc_start_filter = Runnable {
if(! isPageDestroyed && isRegexValid() ) {
if(! isPageDestroyed && isRegexValid()) {
val column = this.column ?: return@Runnable
column.regex_text = etRegexFilter.text.toString()
activity.app_state.saveColumnList()
@ -490,7 +490,7 @@ class ColumnViewHolder(
Column.TYPE_CONVERSATION,
Column.TYPE_INSTANCE_INFORMATION -> refreshLayout.isEnabled = false
Column.TYPE_SEARCH, Column.TYPE_TREND_TAG -> {
Column.TYPE_KEYWORD_FILTER, Column.TYPE_SEARCH, Column.TYPE_TREND_TAG -> {
refreshLayout.isEnabled = true
refreshLayout.direction = SwipyRefreshLayoutDirection.TOP
}
@ -916,17 +916,17 @@ class ColumnViewHolder(
}
override fun onLongClick(v : View) : Boolean {
return when(v.id){
R.id.btnColumnClose-> {
return when(v.id) {
R.id.btnColumnClose -> {
val idx = activity.app_state.column_list.indexOf(column)
activity.closeColumnAll( idx )
activity.closeColumnAll(idx)
true
}
else->false
else -> false
}
}
private fun showError(message : String) {
tvLoading.visibility = View.VISIBLE
tvLoading.text = message
@ -1049,9 +1049,6 @@ class ColumnViewHolder(
}
}
// 表示状態が変わった後にもう一度呼び出す必要があるらしい。。。
// 試しにやめてみる status_adapter.notifyChange()
proc_restoreScrollPosition.run()
}

View File

@ -497,21 +497,26 @@ internal class ItemViewHolder(
private fun showFilter(filter : TootFilter){
llFilter.visibility = View.VISIBLE
tvFilterPhrase.text = filter.phrase
val sb = StringBuffer()
//
sb.append( activity.getString(R.string.filter_context))
.append(": ")
.append(filter.getContextNames(activity).joinToString("/"))
if( filter.irreversible){
sb.append(", ")
.append(activity.getString(R.string.filter_irreversible))
}
//
if( filter.time_expires_at != 0L ){
sb.append(", ")
sb.append('\n')
.append(activity.getString(R.string.filter_expires_at))
.append(": ")
.append( TootStatus.formatTime(activity,filter.time_expires_at,false))
}
//
if( filter.irreversible){
sb.append('\n')
.append(activity.getString(R.string.filter_irreversible))
}
tvFilterDetail.text = sb.toString()
}

View File

@ -73,7 +73,7 @@ class TootFilter( src: JSONObject) :TimelineItem() {
context = parseFilterContext(src.optJSONArray("context"))
expires_at = src.parseString("expires_at") // may null
time_expires_at = TootStatus.parseTime(expires_at)
irreversible = false // FIXME: irreversible flag is not shown in filter API
irreversible = src.optBoolean("irreversible")
}
fun getContextNames(context: Context) : ArrayList<String> {

View File

@ -339,14 +339,14 @@ class TootStatus(parser : TootParser, src : JSONObject) :
reblog?.updateFiltered(muted_words)
}
private fun checkFiltered(muted_words : WordTrieTree?) : Boolean {
muted_words ?: return false
private fun checkFiltered(filter_tree : WordTrieTree?) : Boolean {
filter_tree ?: return false
//
var t = decoded_spoiler_text
if(t.isNotEmpty() && muted_words.matchShort(t)) return true
if(t.isNotEmpty() && filter_tree.matchShort(t)) return true
//
t = decoded_content
if(t.isNotEmpty() && muted_words.matchShort(t)) return true
if(t.isNotEmpty() && filter_tree.matchShort(t)) return true
//
return false
}

View File

@ -6,6 +6,7 @@ class CharacterGroup {
companion object {
// Tokenizerが終端に達したことを示す
const val END = - 1
@ -69,6 +70,13 @@ class CharacterGroup {
// 文字数2: unicode 二つを合成した数値 => group_id。半角カナ濁音など
private val map2 = SparseIntArray()
// ユニコード文字を正規化する。
// 簡易版なので全ての文字には対応していない
fun getUnifiedCharacter(c:Char):Char{
val v1 = map1[c.toInt()]
return if( v1 != 0 ) v1.toChar() else c
}
// グループをmapに登録する
private fun addGroup(list : Array<String>) {

View File

@ -4,11 +4,45 @@ import android.support.v4.util.SparseArrayCompat
import java.util.ArrayList
class WordTrieTree {
class WordTrieTree(private var validator : (src : CharSequence, start : Int, end : Int) -> Boolean = EMPTY_VALIDATOR) {
companion object {
private val grouper = CharacterGroup()
private val EMPTY_VALIDATOR = { _ : CharSequence, _ : Int, _ : Int -> true }
// マストドン2.4.3rc2でキーワードフィルタは単語の前後に 正規表現 \b を仮定するようになった
// Trie木でマッチ候補が出たらマッチ範囲と前後の文字で単語区切りを検証する
val WORD_VALIDATOR = { sequence : CharSequence, start : Int, end : Int ->
// 文字種を正規化してから正規表現の単語構成文字 \w [A-Za-z0-9_] にマッチするか調べる
// 全角半角大文字小文字の違いは吸収されるが、英字数字アンダーバー以外にはマッチしない
fun isWordCharacter(c : Char) : Boolean {
val uc = grouper.getUnifiedCharacter(c)
return when {
'A' <= uc && uc <= 'Z' -> true
'a' <= uc && uc <= 'z' -> true
'0' <= uc && uc <= '9' -> true
uc == '_' -> true
else -> false
}
}
when {
// マッチ範囲の始端とその直前がともに単語構成文字だった場合、\bを満たさない
isWordCharacter(sequence[start])
&& start > 0
&& isWordCharacter(sequence[start - 1]) -> false
// マッチ範囲の終端とその直後がともに単語構成文字だった場合、\bを満たさない
isWordCharacter(sequence[end - 1])
&& end < sequence.length
&& isWordCharacter(sequence[end]) -> false
else -> true
}
}
}
private class Node {
@ -65,7 +99,10 @@ class WordTrieTree {
class Match internal constructor(val start : Int, val end : Int, val word : String)
// Tokenizer が列挙する文字を使って Trie Tree を探索する
private fun match(allowShortMatch : Boolean, t : CharacterGroup.Tokenizer) : Match? {
private fun match(
allowShortMatch : Boolean,
t : CharacterGroup.Tokenizer
) : Match? {
val start = t.offset
var dst : Match? = null
@ -73,12 +110,18 @@ class WordTrieTree {
var node = node_root
while(true) {
// このノードは単語の終端でもある
// match_wordが定義されたードは単語の終端を示す
val match_word = node.match_word
if(match_word != null) {
// マッチ候補はvalidatorで単語区切りなどの検査を行う
if(match_word != null && validator(t.text, start, t.offset)) {
// マッチしたことを覚えておく
dst = Match(start, t.offset, match_word)
// ミュート用途の場合、ひとつでも単語にマッチすればより長い探索は必要ない
if(allowShortMatch) break
// それ以外の場合は最長マッチを探索する
}
val id = t.next()
@ -113,7 +156,7 @@ class WordTrieTree {
}
// ハイライト用。複数マッチする。マッチした位置を覚える
internal fun matchList(src : CharSequence, start : Int, end : Int) : ArrayList<Match>? {
fun matchList(src : CharSequence, start : Int, end : Int) : ArrayList<Match>? {
var dst : ArrayList<Match>? = null

View File

@ -1,140 +1,154 @@
<?xml version="1.0" encoding="utf-8"?>
<ScrollView
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/svContent"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingBottom="128dp"
android:paddingTop="12dp"
android:scrollbarStyle="outsideOverlay"
tools:ignore="TooManyViews"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<LinearLayout
<ScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_height="0dp"
android:layout_weight="1"
android:clipToPadding="false"
android:fillViewport="true"
android:paddingBottom="128dp"
android:paddingTop="12dp"
android:scrollbarStyle="outsideOverlay"
tools:ignore="TooManyViews"
>
<View style="@style/setting_divider"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
>
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etPhrase"
android:text="@string/filter_phrase"
/>
<View style="@style/setting_divider"/>
<LinearLayout style="@style/setting_row_form">
<EditText
android:id="@+id/etPhrase"
style="@style/setting_horizontal_stretch"
android:inputType="text"
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etPhrase"
android:text="@string/filter_phrase"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<View style="@style/setting_divider"/>
<EditText
android:id="@+id/etPhrase"
style="@style/setting_horizontal_stretch"
android:inputType="text"
/>
<TextView
style="@style/setting_row_label"
android:text="@string/filter_context"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<View style="@style/setting_divider"/>
<CheckBox
android:id="@+id/cbContextHome"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_home"
<TextView
style="@style/setting_row_label"
android:text="@string/filter_context"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<LinearLayout style="@style/setting_row_form">
<CheckBox
android:id="@+id/cbContextHome"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_home"
/>
<CheckBox
android:id="@+id/cbContextNotification"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_notification"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<LinearLayout style="@style/setting_row_form">
<CheckBox
android:id="@+id/cbContextNotification"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_notification"
/>
<CheckBox
android:id="@+id/cbContextPublic"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_public"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<LinearLayout style="@style/setting_row_form">
<CheckBox
android:id="@+id/cbContextPublic"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_public"
/>
<CheckBox
android:id="@+id/cbContextThread"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_thread"
/>
</LinearLayout>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<View style="@style/setting_divider"/>
<CheckBox
android:id="@+id/cbContextThread"
style="@style/setting_horizontal_stretch"
android:checked="true"
android:text="@string/filter_thread"
/>
<TextView
style="@style/setting_row_label"
android:text="@string/filter_irreversible"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<View style="@style/setting_divider"/>
<CheckBox
android:id="@+id/cbFilterIrreversible"
style="@style/setting_horizontal_stretch"
<TextView
style="@style/setting_row_label"
android:text="@string/filter_irreversible"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<View style="@style/setting_divider"/>
<CheckBox
android:id="@+id/cbFilterIrreversible"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_irreversible"
/>
<TextView
style="@style/setting_row_label"
android:text="@string/filter_expires_at"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<View style="@style/setting_divider"/>
<TextView
android:id="@+id/tvExpire"
style="@style/setting_horizontal_stretch"
style="@style/setting_row_label"
android:text="@string/filter_expires_at"
/>
<LinearLayout style="@style/setting_row_form">
<TextView
android:id="@+id/tvExpire"
style="@style/setting_horizontal_stretch"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<Spinner
android:id="@+id/spExpire"
style="@style/setting_horizontal_stretch"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<LinearLayout style="@style/setting_row_form">
</LinearLayout>
</LinearLayout>
</ScrollView>
<LinearLayout style="@style/setting_row_form">
<Spinner
android:id="@+id/spExpire"
style="@style/setting_horizontal_stretch"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<LinearLayout style="@style/setting_row_form">
<Button
android:id="@+id/btnSave"
style="@style/setting_horizontal_stretch"
android:text="@string/save"
/>
</LinearLayout>
</LinearLayout>
</ScrollView>
<Button
android:id="@+id/btnSave"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/save"
/>
</LinearLayout>