- 単語フィルタ編集画面の初期状態でソフトキーボードを表示しない
- 期限を指定したフィルタを後から無期限にする方法がないので、秒数にInt.MAX_VALUE shr 1 を渡す - 単語フィルタ一覧カラムで上端スワイプするとリロード - 単語フィルタ一覧の項目表示を少しキレイにした - 単語フィルタ一覧で不可逆フラグを表示する - 単語フィルタは単語[A-Za-z0-9_]の区切りを意識したマッチングを行う - 単語フィルタの作成で適用箇所の初期状態を全てチェック済みに変更 - 単語フィルタの編集画面で保存ボタンをスクロールビューの外側に配置
This commit is contained in:
parent
218b832587
commit
eccee68092
|
@ -190,6 +190,7 @@
|
|||
<activity
|
||||
android:name=".ActKeywordFilter"
|
||||
android:label="@string/keyword_filter_new"
|
||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden"
|
||||
/>
|
||||
|
||||
<activity
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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>) {
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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>
|
Loading…
Reference in New Issue