キーワードフィルタを単語マッチに対応。アプリ設定に「カラムストリップのタップで上端にスクロール」を追加

This commit is contained in:
tateisu 2018-07-10 15:44:34 +09:00
parent eccee68092
commit 8f66511c46
15 changed files with 173 additions and 62 deletions

View File

@ -12,8 +12,8 @@ android {
minSdkVersion 21
targetSdkVersion 27
versionCode 262
versionName "2.6.2"
versionCode 263
versionName "2.6.3"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
// https://stackoverflow.com/questions/47791227/java-lang-illegalstateexception-dex-archives-setting-dex-extension-only-for

View File

@ -42,6 +42,7 @@ class ActKeywordFilter
}
internal const val STATE_EXPIRE_SPINNER = "expire_spinner"
internal const val STATE_EXPIRE_AT = "expire_at"
private val expire_duration_list = arrayOf(
- 1, // dont change
@ -64,12 +65,14 @@ class ActKeywordFilter
private lateinit var cbContextPublic : CheckBox
private lateinit var cbContextThread : CheckBox
private lateinit var cbFilterIrreversible : CheckBox
private lateinit var cbFilterWordMatch : CheckBox
private lateinit var tvExpire : TextView
private lateinit var spExpire : Spinner
private var loading = false
private var density : Float = 1f
private var filter_id : Long = - 1L
private var filter_expire : Long = 0L
///////////////////////////////////////////////////
@ -107,6 +110,7 @@ class ActKeywordFilter
if(iv != - 1) {
spExpire.setSelection(iv)
}
filter_expire = savedInstanceState.getLong(STATE_EXPIRE_AT, filter_expire)
}
}
@ -114,6 +118,7 @@ class ActKeywordFilter
super.onSaveInstanceState(outState)
if(! loading) {
outState.putInt(STATE_EXPIRE_SPINNER, spExpire.selectedItemPosition)
outState.putLong(STATE_EXPIRE_AT,filter_expire)
}
}
@ -132,6 +137,7 @@ class ActKeywordFilter
cbContextPublic = findViewById(R.id.cbContextPublic)
cbContextThread = findViewById(R.id.cbContextThread)
cbFilterIrreversible = findViewById(R.id.cbFilterIrreversible)
cbFilterWordMatch = findViewById(R.id.cbFilterWordMatch)
tvExpire = findViewById(R.id.tvExpire)
spExpire = findViewById(R.id.spExpire)
@ -183,6 +189,8 @@ class ActKeywordFilter
private fun onLoadComplete(filter : TootFilter) {
loading = false
filter_expire = filter.time_expires_at
etPhrase.setText(filter.phrase)
setContextChecked(filter, cbContextHome, TootFilter.CONTEXT_HOME)
setContextChecked(filter, cbContextNotification, TootFilter.CONTEXT_NOTIFICATIONS)
@ -190,6 +198,7 @@ class ActKeywordFilter
setContextChecked(filter, cbContextThread, TootFilter.CONTEXT_THREAD)
cbFilterIrreversible.isChecked = filter.irreversible
cbFilterWordMatch.isChecked = filter.whole_word
tvExpire.text = if(filter.time_expires_at == 0L) {
getString(R.string.filter_expire_unlimited)
@ -227,6 +236,7 @@ class ActKeywordFilter
})
put("irreversible", cbFilterIrreversible.isChecked)
put("whole_word", cbFilterWordMatch.isChecked)
var seconds = - 1
@ -238,15 +248,14 @@ class ActKeywordFilter
when(seconds) {
// dont change
- 1 -> put("expires_in", "")
- 1 -> {}
// unlimited
0 -> if(filter_id == - 1L) {
// don't specify expires_in for creating
put("expires_in", "")
0 -> if( filter_expire <= 0L ) {
// already unlimited. don't change.
} else {
// FIXME: currently there is no way to remove expires from existing filter.
put("expires_in", (Int.MAX_VALUE shr 1))
put("expires_in", Int.MAX_VALUE )
}
// set seconds

View File

@ -1009,7 +1009,7 @@ class ActMain : AppCompatActivity()
false,
Column.TYPE_BLOCKS
)
R.id.nav_keyword_filter-> Action_Account.timeline(
R.id.nav_keyword_filter -> Action_Account.timeline(
this,
defaultInsertPosition,
false,
@ -1314,6 +1314,18 @@ class ActMain : AppCompatActivity()
})
}
private fun isVisibleColumn(idx : Int) = phoneTab({ env ->
val c = env.pager.currentItem
c == idx
}, { env ->
val vs = env.tablet_layout_manager.findFirstVisibleItemPosition()
var ve = env.tablet_layout_manager.findLastVisibleItemPosition()
if(ve - vs > nScreenColumn - 1) {
ve = vs + nScreenColumn - 1
}
vs != RecyclerView.NO_POSITION && vs <= idx && idx <= ve
})
internal fun updateColumnStrip() {
llEmpty.visibility = if(app_state.column_list.isEmpty()) View.VISIBLE else View.GONE
@ -1326,7 +1338,14 @@ class ActMain : AppCompatActivity()
val ivIcon = viewRoot.findViewById<ImageView>(R.id.ivIcon)
viewRoot.tag = i
viewRoot.setOnClickListener { v -> scrollToColumn(v.tag as Int) }
viewRoot.setOnClickListener { v ->
val idx = v.tag as Int
if(Pref.bpScrollTopFromColumnStrip(pref) && isVisibleColumn(idx)) {
app_state.column_list[i].viewHolder?.scrollToTop2()
return@setOnClickListener
}
scrollToColumn(idx)
}
viewRoot.contentDescription = column.getColumnName(true)
//

View File

@ -3991,10 +3991,13 @@ class Column(
private fun encodeFilterTree(filterList : ArrayList<TootFilter>?) : WordTrieTree? {
val column_context = getFilterContext()
if(column_context == 0 || filterList == null) return null
val tree = WordTrieTree(WordTrieTree.WORD_VALIDATOR)
val tree = WordTrieTree()
for(filter in filterList) {
if((filter.context and column_context) != 0) {
tree.add(filter.phrase)
tree.add(filter.phrase,validator = when(filter.whole_word){
true -> WordTrieTree.WORD_VALIDATOR
else -> WordTrieTree.EMPTY_VALIDATOR
})
}
}
return tree

View File

@ -851,6 +851,8 @@ class ColumnViewHolder(
}
}
override fun onClick(v : View) {
val column = this.column
val status_adapter = this.status_adapter
@ -883,11 +885,7 @@ class ColumnViewHolder(
column.startLoading()
}
R.id.llColumnHeader -> {
if(status_adapter.itemCount > 0) {
scrollToTop()
}
}
R.id.llColumnHeader -> scrollToTop2()
R.id.btnColumnSetting -> llColumnSetting.visibility =
if(llColumnSetting.visibility == View.VISIBLE) View.GONE else View.VISIBLE
@ -1210,5 +1208,12 @@ class ColumnViewHolder(
} catch(ignored : Throwable) {
}
}
fun scrollToTop2() {
val status_adapter = this.status_adapter
if(loading_busy || status_adapter == null) return
if(status_adapter.itemCount > 0) {
scrollToTop()
}
}
}

View File

@ -504,18 +504,20 @@ internal class ItemViewHolder(
.append(": ")
.append(filter.getContextNames(activity).joinToString("/"))
//
val flags = ArrayList<String>()
if( filter.irreversible ) flags.add(activity.getString(R.string.filter_irreversible))
if( filter.whole_word ) flags.add(activity.getString(R.string.filter_word_match))
if( flags.isNotEmpty() ){
sb.append('\n')
.append( flags.joinToString(", "))
}
//
if( filter.time_expires_at != 0L ){
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

@ -312,7 +312,11 @@ object Pref {
false,
R.id.swDontRetrievePreviewCard
)
val bpScrollTopFromColumnStrip = BooleanPref(
"ScrollTopFromColumnStrip",
false,
R.id.swScrollTopFromColumnStrip
)
// int

View File

@ -66,6 +66,7 @@ class TootFilter( src: JSONObject) :TimelineItem() {
private val expires_at : String? // null is not specified, or "2018-07-06T00:59:13.161Z"
val time_expires_at : Long // 0L if not specified
val irreversible : Boolean
val whole_word : Boolean
init{
id = src.parseLong("id") ?: throw RuntimeException("missing id")
@ -74,6 +75,7 @@ class TootFilter( src: JSONObject) :TimelineItem() {
expires_at = src.parseString("expires_at") // may null
time_expires_at = TootStatus.parseTime(expires_at)
irreversible = src.optBoolean("irreversible")
whole_word = src.optBoolean("whole_word")
}
fun getContextNames(context: Context) : ArrayList<String> {

View File

@ -6,7 +6,6 @@ class CharacterGroup {
companion object {
// Tokenizerが終端に達したことを示す
const val END = - 1

View File

@ -4,13 +4,13 @@ import android.support.v4.util.SparseArrayCompat
import java.util.ArrayList
class WordTrieTree(private var validator : (src : CharSequence, start : Int, end : Int) -> Boolean = EMPTY_VALIDATOR) {
class WordTrieTree {
companion object {
private val grouper = CharacterGroup()
private val EMPTY_VALIDATOR = { _ : CharSequence, _ : Int, _ : Int -> true }
val EMPTY_VALIDATOR = { _ : CharSequence, _ : Int, _ : Int -> true }
// マストドン2.4.3rc2でキーワードフィルタは単語の前後に 正規表現 \b を仮定するようになった
// Trie木でマッチ候補が出たらマッチ範囲と前後の文字で単語区切りを検証する
@ -55,6 +55,10 @@ class WordTrieTree(private var validator : (src : CharSequence, start : Int, end
// Trieツリー的には終端単語と続くードの両方が存在する場合がありうる。
// たとえば ABC と ABCDEF を登録してから ABCDEFG を探索したら、単語 ABC と単語 ABCDEF にマッチする。
// このノードが終端なら、単語マッチの有無を覚えておく
internal var validator : (src : CharSequence, start : Int, end : Int) -> Boolean =
EMPTY_VALIDATOR
}
private val node_root = Node()
@ -63,7 +67,10 @@ class WordTrieTree(private var validator : (src : CharSequence, start : Int, end
get() = node_root.child_nodes.size() == 0
// 単語の追加
fun add(s : String) {
fun add(
s : String,
validator : (src : CharSequence, start : Int, end : Int) -> Boolean = EMPTY_VALIDATOR
) {
val t = grouper.tokenizer().reset(s, 0, s.length)
var token_count = 0
@ -80,6 +87,7 @@ class WordTrieTree(private var validator : (src : CharSequence, start : Int, end
val old_word = node.match_word
if(old_word == null || old_word.length < t.text.length) {
node.match_word = t.text.toString()
node.validator = validator
}
return
@ -113,7 +121,9 @@ class WordTrieTree(private var validator : (src : CharSequence, start : Int, end
// match_wordが定義されたードは単語の終端を示す
val match_word = node.match_word
// マッチ候補はvalidatorで単語区切りなどの検査を行う
if(match_word != null && validator(t.text, start, t.offset)) {
if(match_word != null
&& node.validator(t.text, start, t.offset)
) {
// マッチしたことを覚えておく
dst = Match(start, t.offset, match_word)

View File

@ -218,6 +218,22 @@
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/scroll_top_from_column_strip"
/>
<LinearLayout style="@style/setting_row_form">
<Switch
android:id="@+id/swScrollTopFromColumnStrip"
style="@style/setting_horizontal_stretch"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/dont_screen_off"

View File

@ -100,7 +100,7 @@
<TextView
style="@style/setting_row_label"
android:text="@string/filter_irreversible"
android:text="@string/filter_options"
/>
<LinearLayout style="@style/setting_row_form">
@ -108,11 +108,19 @@
<CheckBox
android:id="@+id/cbFilterIrreversible"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_irreversible"
android:text="@string/filter_irreversible_long"
/>
</LinearLayout>
<LinearLayout style="@style/setting_row_form">
<CheckBox
android:id="@+id/cbFilterWordMatch"
style="@style/setting_horizontal_stretch"
android:text="@string/filter_word_match_long"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView

View File

@ -692,18 +692,31 @@
<string name="follow_suggestion">Follow suggestion</string>
<string name="close_all_columns">Close all columns</string>
<string name="confirm_close_column_all">All columns (exclude protected) will be closed. Are tou sure?</string>
<string name="filtered">filtered</string>
<string name="keyword_filters">Keyword filters</string>
<string name="filter_context">context</string>
<string name="filter_expires_at">expires_at</string>
<string name="filter_home">home</string>
<string name="keyword_filters">Keyword filters</string>
<string name="keyword_filter">Keyword filter</string>
<string name="keyword_filter_new">New filter</string>
<string name="keyword_filter_edit">Edit filter</string>
<string name="filter_delete_confirm">The filter \'%1$s\' will be deleted. Are you sure?</string>
<string name="filter_of">Filter \'%1$s\'</string>
<string name="filtered">filtered</string>
<string name="filter_phrase">Phrase</string>
<string name="filter_context">Filter contexts</string>
<string name="filter_options">Options</string>
<string name="filter_irreversible">Irreversible</string>
<string name="filter_irreversible_long">Irreversible. Drop instead of hide. Filtered toots will disappear irreversibly, even if filter is later removed.</string>
<string name="filter_word_match">Whole word</string>
<string name="filter_word_match_long">Whole word. Recommended to enable only phrases consisting of alphanumeric characters.</string>
<string name="filter_expires_at">Expire</string>
<string name="filter_home">home</string>
<string name="filter_notification">notification</string>
<string name="filter_public">public</string>
<string name="filter_thread">conversation</string>
<string name="filter_irreversible">irreversible</string>
<string name="filter_delete_confirm">The filter \'%1$s\' will be deleted. Are you sure?</string>
<string name="filter_of">Filter \'%1$s\'</string>
<string name="filter_phrase">Phrase</string>
<string name="dont_change">Don\'t change</string>
<string name="filter_expire_unlimited">Unlimited</string>
<string name="filter_expire_30min">30 minutes after save</string>
@ -712,11 +725,9 @@
<string name="filter_expire_12hour">12 hours after save</string>
<string name="filter_expire_1day">1 day after save</string>
<string name="filter_expire_1week">1 week after save</string>
<string name="keyword_filter_new">New filter</string>
<string name="keyword_filter_edit">Edit filter</string>
<string name="keyword_filter">Keyword filter</string>
<string name="scroll_top_from_column_strip">Scroll to top when tapping visible column in column strip</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</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>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->

View File

@ -970,19 +970,31 @@
<string name="follow_suggestion">フォロー推奨ユーザ</string>
<string name="close_all_columns">全てのカラムを閉じる</string>
<string name="confirm_close_column_all">(保護されていない)全てのカラムが閉じられます。よろしいですか?</string>
<string name="filtered">フィルタされました</string>
<string name="keyword_filter">単語フィルタ</string>
<string name="keyword_filters">単語フィルタ</string>
<string name="keyword_filter">単語フィルタ</string>
<string name="keyword_filter_new">フィルタの作成</string>
<string name="keyword_filter_edit">フィルタの編集</string>
<string name="filter_delete_confirm">フィルタ \'%1$s\' は削除されます。よろしいですか?</string>
<string name="filter_of">フィルタ \'%1$s\'</string>
<string name="filtered">フィルタされました</string>
<string name="filter_phrase">フレーズ</string>
<string name="filter_context">適用箇所</string>
<string name="filter_options">オプション</string>
<string name="filter_irreversible">不可逆</string>
<string name="filter_irreversible_long">不可逆。隠すのではなく除外する。フィルターが後で削除されても、除外されたトゥートは元に戻せなくなります。</string>
<string name="filter_word_match">単語マッチ</string>
<string name="filter_word_match_long">単語マッチ。英数字だけで構成されるフレーズ以外では無効にすることを推奨します。</string>
<string name="filter_expires_at">期限</string>
<string name="filter_home">ホーム</string>
<string name="filter_notification">通知</string>
<string name="filter_public">公開TL</string>
<string name="filter_thread">会話</string>
<string name="filter_irreversible">不可逆</string>
<string name="filter_delete_confirm">フィルタ \'%1$s\' は削除されます。よろしいですか?</string>
<string name="filter_of">フィルタ \'%1$s\'</string>
<string name="filter_phrase">フレーズ</string>
<string name="dont_change">変更しない</string>
<string name="filter_expire_unlimited">無期限</string>
<string name="filter_expire_30min">30分後</string>
@ -991,7 +1003,6 @@
<string name="filter_expire_12hour">12時間後</string>
<string name="filter_expire_1day">1日後</string>
<string name="filter_expire_1week">1週間後</string>
<string name="keyword_filter_new">フィルタの作成</string>
<string name="keyword_filter_edit">フィルタの編集</string>
<string name="scroll_top_from_column_strip">Scroll to top when tapping visible column in column strip</string>
</resources>

View File

@ -677,18 +677,32 @@
<string name="follow_suggestion">Follow suggestion</string>
<string name="close_all_columns">Close all columns</string>
<string name="confirm_close_column_all">All columns (exclude protected) will be closed. Are tou sure?</string>
<string name="filtered">filtered</string>
<string name="keyword_filters">Keyword filters</string>
<string name="filter_context">context</string>
<string name="filter_expires_at">expires_at</string>
<string name="keyword_filter">Keyword filter</string>
<string name="keyword_filter_new">New filter</string>
<string name="keyword_filter_edit">Edit filter</string>
<string name="filter_delete_confirm">The filter \'%1$s\' will be deleted. Are you sure?</string>
<string name="filter_of">Filter \'%1$s\'</string>
<string name="filtered">filtered</string>
<string name="filter_phrase">Phrase</string>
<string name="filter_context">Filter contexts</string>
<string name="filter_options">Options</string>
<string name="filter_irreversible">Irreversible</string>
<string name="filter_irreversible_long">Irreversible. Drop instead of hide. Filtered toots will disappear irreversibly, even if filter is later removed.</string>
<string name="filter_word_match">Whole word</string>
<string name="filter_word_match_long">Whole word. Recommended to enable only phrases consisting of alphanumeric characters.</string>
<string name="filter_expires_at">Expire</string>
<string name="filter_home">home</string>
<string name="filter_notification">notification</string>
<string name="filter_public">public</string>
<string name="filter_thread">conversation</string>
<string name="filter_irreversible">irreversible</string>
<string name="filter_delete_confirm">The filter \'%1$s\' will be deleted. Are you sure?</string>
<string name="filter_of">Filter \'%1$s\'</string>
<string name="filter_phrase">Phrase</string>
<string name="dont_change">Don\'t change</string>
<string name="filter_expire_unlimited">Unlimited</string>
<string name="filter_expire_30min">30 minutes after save</string>
@ -697,7 +711,5 @@
<string name="filter_expire_12hour">12 hours after save</string>
<string name="filter_expire_1day">1 day after save</string>
<string name="filter_expire_1week">1 week after save</string>
<string name="keyword_filter_new">New filter</string>
<string name="keyword_filter_edit">Edit filter</string>
<string name="keyword_filter">Keyword filter</string>
<string name="scroll_top_from_column_strip">Scroll to top when tapping visible column in column strip</string>
</resources>