mirror of
https://github.com/tateisu/SubwayTooter
synced 2025-02-04 12:47:48 +01:00
ColumnViewHolderクラスのメソッドを拡張関数として別ファイルに移動する
This commit is contained in:
parent
aa29a1e5ec
commit
eaed84d8a4
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,299 @@
|
||||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import jp.juggler.subwaytooter.action.Action_Account
|
||||
import jp.juggler.subwaytooter.action.Action_List
|
||||
import jp.juggler.subwaytooter.action.Action_Notification
|
||||
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
|
||||
import jp.juggler.util.hideKeyboard
|
||||
import jp.juggler.util.isCheckedNoAnime
|
||||
import jp.juggler.util.showToast
|
||||
import jp.juggler.util.withCaption
|
||||
import java.util.regex.Pattern
|
||||
|
||||
fun ColumnViewHolder.onListListUpdated() {
|
||||
etListName.setText("")
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.checkRegexFilterError(src: String): String? {
|
||||
try {
|
||||
if (src.isEmpty()) {
|
||||
return null
|
||||
}
|
||||
val m = Pattern.compile(src).matcher("")
|
||||
if (m.find()) {
|
||||
// 空文字列にマッチする正規表現はエラー扱いにする
|
||||
// そうしないとCWの警告テキストにマッチしてしまう
|
||||
return activity.getString(R.string.regex_filter_matches_empty_string)
|
||||
}
|
||||
return null
|
||||
} catch (ex: Throwable) {
|
||||
val message = ex.message
|
||||
return if (message != null && message.isNotEmpty()) {
|
||||
message
|
||||
} else {
|
||||
ex.withCaption(activity.resources, R.string.regex_error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.isRegexValid(): Boolean {
|
||||
val s = etRegexFilter.text.toString()
|
||||
val error = checkRegexFilterError(s)
|
||||
tvRegexFilterError.text = error ?: ""
|
||||
return error == null
|
||||
}
|
||||
|
||||
|
||||
fun ColumnViewHolder.onCheckedChangedImpl(view: CompoundButton?, isChecked: Boolean) {
|
||||
view ?: return
|
||||
|
||||
val column = this.column
|
||||
|
||||
if (binding_busy || column == null || status_adapter == null) return
|
||||
|
||||
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
|
||||
// リロードやリフレッシュ操作で直るようにする
|
||||
column.addColumnViewHolder(this)
|
||||
|
||||
when (view) {
|
||||
|
||||
cbDontCloseColumn -> {
|
||||
column.dont_close = isChecked
|
||||
showColumnCloseButton()
|
||||
activity.app_state.saveColumnList()
|
||||
}
|
||||
|
||||
cbWithAttachment -> {
|
||||
column.with_attachment = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbRemoteOnly -> {
|
||||
column.remote_only = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbWithHighlight -> {
|
||||
column.with_highlight = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowBoost -> {
|
||||
column.dont_show_boost = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowReply -> {
|
||||
column.dont_show_reply = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowReaction -> {
|
||||
column.dont_show_reaction = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowVote -> {
|
||||
column.dont_show_vote = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowNormalToot -> {
|
||||
column.dont_show_normal_toot = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowNonPublicToot -> {
|
||||
column.dont_show_non_public_toot = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowFavourite -> {
|
||||
column.dont_show_favourite = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontShowFollow -> {
|
||||
column.dont_show_follow = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbInstanceLocal -> {
|
||||
column.instance_local = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
cbDontStreaming -> {
|
||||
column.dont_streaming = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
activity.app_state.streamManager.updateStreamingColumns()
|
||||
}
|
||||
|
||||
cbDontAutoRefresh -> {
|
||||
column.dont_auto_refresh = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
}
|
||||
|
||||
cbHideMediaDefault -> {
|
||||
column.hide_media_default = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.fireShowContent(reason = "HideMediaDefault in ColumnSetting", reset = true)
|
||||
}
|
||||
|
||||
cbSystemNotificationNotRelated -> {
|
||||
column.system_notification_not_related = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
}
|
||||
|
||||
cbEnableSpeech -> {
|
||||
column.enable_speech = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
}
|
||||
|
||||
cbOldApi -> {
|
||||
column.use_old_api = isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.onClickImpl(v: View?) {
|
||||
v?: return
|
||||
|
||||
val column = this.column
|
||||
val status_adapter = this.status_adapter
|
||||
if (binding_busy || column == null || status_adapter == null) return
|
||||
|
||||
// カラムを追加/削除したときに ColumnからColumnViewHolderへの参照が外れることがある
|
||||
// リロードやリフレッシュ操作で直るようにする
|
||||
column.addColumnViewHolder(this)
|
||||
|
||||
when (v) {
|
||||
btnColumnClose -> activity.closeColumn(column)
|
||||
|
||||
btnColumnReload -> {
|
||||
App1.custom_emoji_cache.clearErrorCache()
|
||||
|
||||
if (column.isSearchColumn) {
|
||||
etSearch.hideKeyboard()
|
||||
etSearch.setText(column.search_query)
|
||||
cbResolve.isCheckedNoAnime = column.search_resolve
|
||||
}
|
||||
refreshLayout.isRefreshing = false
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
btnSearch -> {
|
||||
etSearch.hideKeyboard()
|
||||
column.search_query = etSearch.text.toString().trim { it <= ' ' }
|
||||
column.search_resolve = cbResolve.isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
btnSearchClear -> {
|
||||
etSearch.setText("")
|
||||
column.search_query = ""
|
||||
column.search_resolve = cbResolve.isChecked
|
||||
activity.app_state.saveColumnList()
|
||||
column.startLoading()
|
||||
}
|
||||
|
||||
llColumnHeader -> scrollToTop2()
|
||||
|
||||
btnColumnSetting -> {
|
||||
if (showColumnSetting(!isColumnSettingShown)) {
|
||||
hideAnnouncements()
|
||||
}
|
||||
}
|
||||
|
||||
btnDeleteNotification -> Action_Notification.deleteAll(
|
||||
activity,
|
||||
column.access_info,
|
||||
false
|
||||
)
|
||||
|
||||
btnColor ->
|
||||
activity.app_state.columnIndex(column)?.let {
|
||||
ActColumnCustomize.open(activity, it, ActMain.REQUEST_CODE_COLUMN_COLOR)
|
||||
}
|
||||
|
||||
btnLanguageFilter ->
|
||||
activity.app_state.columnIndex(column)?.let {
|
||||
ActLanguageFilter.open(activity, it, ActMain.REQUEST_CODE_LANGUAGE_FILTER)
|
||||
}
|
||||
|
||||
btnListAdd -> {
|
||||
val tv = etListName.text.toString().trim { it <= ' ' }
|
||||
if (tv.isEmpty()) {
|
||||
activity.showToast(true, R.string.list_name_empty)
|
||||
return
|
||||
}
|
||||
Action_List.create(activity, column.access_info, tv, null)
|
||||
}
|
||||
|
||||
llRefreshError -> {
|
||||
column.mRefreshLoadingErrorPopupState = 1 - column.mRefreshLoadingErrorPopupState
|
||||
showRefreshError()
|
||||
}
|
||||
|
||||
btnQuickFilterAll -> clickQuickFilter(Column.QUICK_FILTER_ALL)
|
||||
btnQuickFilterMention -> clickQuickFilter(Column.QUICK_FILTER_MENTION)
|
||||
btnQuickFilterFavourite -> clickQuickFilter(Column.QUICK_FILTER_FAVOURITE)
|
||||
btnQuickFilterBoost -> clickQuickFilter(Column.QUICK_FILTER_BOOST)
|
||||
btnQuickFilterFollow -> clickQuickFilter(Column.QUICK_FILTER_FOLLOW)
|
||||
btnQuickFilterPost -> clickQuickFilter(Column.QUICK_FILTER_POST)
|
||||
btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION)
|
||||
btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE)
|
||||
|
||||
btnAnnouncements -> toggleAnnouncements()
|
||||
|
||||
btnAnnouncementsPrev -> {
|
||||
column.announcementId =
|
||||
TootAnnouncement.move(column.announcements, column.announcementId, -1)
|
||||
activity.app_state.saveColumnList()
|
||||
showAnnouncements()
|
||||
}
|
||||
|
||||
btnAnnouncementsNext -> {
|
||||
column.announcementId =
|
||||
TootAnnouncement.move(column.announcements, column.announcementId, +1)
|
||||
activity.app_state.saveColumnList()
|
||||
showAnnouncements()
|
||||
}
|
||||
|
||||
btnConfirmMail -> {
|
||||
Action_Account.resendConfirmMail(activity, column.access_info)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.onLongClickImpl(v: View?): Boolean {
|
||||
v?: return false
|
||||
return when (v) {
|
||||
btnColumnClose ->
|
||||
activity.app_state.columnIndex(column)?.let {
|
||||
activity.closeColumnAll(it)
|
||||
true
|
||||
} ?: false
|
||||
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,451 @@
|
||||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.os.SystemClock
|
||||
import android.text.Spannable
|
||||
import android.text.SpannableStringBuilder
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Button
|
||||
import android.widget.ImageButton
|
||||
import android.widget.ImageView
|
||||
import android.widget.LinearLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.google.android.flexbox.FlexWrap
|
||||
import com.google.android.flexbox.FlexboxLayout
|
||||
import com.google.android.flexbox.JustifyContent
|
||||
import jp.juggler.emoji.UnicodeEmoji
|
||||
import jp.juggler.subwaytooter.api.TootApiClient
|
||||
import jp.juggler.subwaytooter.api.TootApiResult
|
||||
import jp.juggler.subwaytooter.api.TootTask
|
||||
import jp.juggler.subwaytooter.api.TootTaskRunner
|
||||
import jp.juggler.subwaytooter.api.entity.CustomEmoji
|
||||
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
|
||||
import jp.juggler.subwaytooter.api.entity.TootReaction
|
||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||
import jp.juggler.subwaytooter.dialog.EmojiPicker
|
||||
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
|
||||
import jp.juggler.subwaytooter.util.*
|
||||
import jp.juggler.util.*
|
||||
import org.jetbrains.anko.allCaps
|
||||
import org.jetbrains.anko.padding
|
||||
import org.jetbrains.anko.textColor
|
||||
|
||||
|
||||
fun ColumnViewHolder.hideAnnouncements() {
|
||||
val column = column ?: return
|
||||
|
||||
if (column.announcementHideTime <= 0L)
|
||||
column.announcementHideTime = System.currentTimeMillis()
|
||||
activity.app_state.saveColumnList()
|
||||
showAnnouncements()
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.toggleAnnouncements() {
|
||||
val column = column ?: return
|
||||
|
||||
if (llAnnouncementsBox.visibility == View.VISIBLE) {
|
||||
if (column.announcementHideTime <= 0L)
|
||||
column.announcementHideTime = System.currentTimeMillis()
|
||||
} else {
|
||||
showColumnSetting(false)
|
||||
column.announcementHideTime = 0L
|
||||
}
|
||||
activity.app_state.saveColumnList()
|
||||
showAnnouncements()
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showAnnouncements(force: Boolean = true) {
|
||||
val column = column ?: return
|
||||
|
||||
if (!force && lastAnnouncementShown >= column.announcementUpdated) {
|
||||
return
|
||||
}
|
||||
lastAnnouncementShown = SystemClock.elapsedRealtime()
|
||||
|
||||
fun clearExtras() {
|
||||
for (invalidator in extra_invalidator_list) {
|
||||
invalidator.register(null)
|
||||
}
|
||||
extra_invalidator_list.clear()
|
||||
}
|
||||
llAnnouncementExtra.removeAllViews()
|
||||
clearExtras()
|
||||
|
||||
val listShown = TootAnnouncement.filterShown(column.announcements)
|
||||
if (listShown?.isEmpty() != false) {
|
||||
btnAnnouncements.vg(false)
|
||||
llAnnouncementsBox.vg(false)
|
||||
btnAnnouncementsBadge.vg(false)
|
||||
llColumnHeader.invalidate()
|
||||
return
|
||||
}
|
||||
|
||||
btnAnnouncements.vg(true)
|
||||
|
||||
val expand = column.announcementHideTime <= 0L
|
||||
|
||||
llAnnouncementsBox.vg(expand)
|
||||
llColumnHeader.invalidate()
|
||||
|
||||
btnAnnouncementsBadge.vg(false)
|
||||
if (!expand) {
|
||||
val newer = listShown.find { it.updated_at > column.announcementHideTime }
|
||||
if (newer != null) {
|
||||
column.announcementId = newer.id
|
||||
btnAnnouncementsBadge.vg(true)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val content_color = column.getContentColor()
|
||||
|
||||
val item = listShown.find { it.id == column.announcementId }
|
||||
?: listShown[0]
|
||||
|
||||
val itemIndex = listShown.indexOf(item)
|
||||
|
||||
val enablePaging = listShown.size > 1
|
||||
|
||||
val alphaPrevNext = if (enablePaging) 1f else 0.3f
|
||||
|
||||
setIconDrawableId(
|
||||
activity,
|
||||
btnAnnouncementsPrev,
|
||||
R.drawable.ic_arrow_start,
|
||||
color = content_color,
|
||||
alphaMultiplier = alphaPrevNext
|
||||
)
|
||||
|
||||
setIconDrawableId(
|
||||
activity,
|
||||
btnAnnouncementsNext,
|
||||
R.drawable.ic_arrow_end,
|
||||
color = content_color,
|
||||
alphaMultiplier = alphaPrevNext
|
||||
)
|
||||
|
||||
|
||||
btnAnnouncementsPrev.vg(expand)?.run {
|
||||
isEnabled = enablePaging
|
||||
}
|
||||
btnAnnouncementsNext.vg(expand)?.run {
|
||||
isEnabled = enablePaging
|
||||
}
|
||||
|
||||
tvAnnouncementsCaption.textColor = content_color
|
||||
tvAnnouncementsIndex.textColor = content_color
|
||||
tvAnnouncementPeriod.textColor = content_color
|
||||
|
||||
val f = activity.timeline_font_size_sp
|
||||
if (!f.isNaN()) {
|
||||
tvAnnouncementsCaption.textSize = f
|
||||
tvAnnouncementsIndex.textSize = f
|
||||
tvAnnouncementPeriod.textSize = f
|
||||
tvAnnouncementContent.textSize = f
|
||||
}
|
||||
val spacing = activity.timeline_spacing
|
||||
if (spacing != null) {
|
||||
tvAnnouncementPeriod.setLineSpacing(0f, spacing)
|
||||
tvAnnouncementContent.setLineSpacing(0f, spacing)
|
||||
}
|
||||
tvAnnouncementsCaption.typeface = ActMain.timeline_font_bold
|
||||
val font_normal = ActMain.timeline_font
|
||||
tvAnnouncementsIndex.typeface = font_normal
|
||||
tvAnnouncementPeriod.typeface = font_normal
|
||||
tvAnnouncementContent.typeface = font_normal
|
||||
|
||||
tvAnnouncementsIndex.vg(expand)?.text =
|
||||
activity.getString(R.string.announcements_index, itemIndex + 1, listShown.size)
|
||||
llAnnouncements.vg(expand)
|
||||
|
||||
var periods: StringBuilder? = null
|
||||
fun String.appendPeriod() {
|
||||
val sb = periods
|
||||
if (sb == null) {
|
||||
periods = StringBuilder(this)
|
||||
} else {
|
||||
sb.append("\n")
|
||||
sb.append(this)
|
||||
}
|
||||
}
|
||||
|
||||
val (strStart, strEnd) = TootStatus.formatTimeRange(
|
||||
item.starts_at,
|
||||
item.ends_at,
|
||||
item.all_day
|
||||
)
|
||||
|
||||
when {
|
||||
|
||||
// no periods.
|
||||
strStart == "" && strEnd == "" -> {
|
||||
}
|
||||
|
||||
// single date
|
||||
strStart == strEnd -> {
|
||||
activity.getString(R.string.announcements_period1, strStart)
|
||||
.appendPeriod()
|
||||
}
|
||||
|
||||
else -> {
|
||||
activity.getString(R.string.announcements_period2, strStart, strEnd)
|
||||
.appendPeriod()
|
||||
}
|
||||
}
|
||||
|
||||
if (item.updated_at > item.published_at) {
|
||||
val strUpdateAt = TootStatus.formatTime(activity, item.updated_at, false)
|
||||
activity.getString(R.string.edited_at, strUpdateAt).appendPeriod()
|
||||
}
|
||||
|
||||
val sb = periods
|
||||
tvAnnouncementPeriod.vg(sb != null)?.text = sb
|
||||
|
||||
tvAnnouncementContent.textColor = content_color
|
||||
tvAnnouncementContent.text = item.decoded_content
|
||||
tvAnnouncementContent.tag = this@showAnnouncements
|
||||
announcementContentInvalidator.register(item.decoded_content)
|
||||
|
||||
// リアクションの表示
|
||||
|
||||
val density = activity.density
|
||||
|
||||
val buttonHeight = ActMain.boostButtonSize
|
||||
val marginBetween = (buttonHeight.toFloat() * 0.2f + 0.5f).toInt()
|
||||
val marginBottom = (buttonHeight.toFloat() * 0.2f + 0.5f).toInt()
|
||||
|
||||
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
|
||||
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
|
||||
|
||||
val box = FlexboxLayout(activity).apply {
|
||||
flexWrap = FlexWrap.WRAP
|
||||
justifyContent = JustifyContent.FLEX_START
|
||||
layoutParams = LinearLayout.LayoutParams(
|
||||
LinearLayout.LayoutParams.MATCH_PARENT,
|
||||
LinearLayout.LayoutParams.WRAP_CONTENT
|
||||
).apply {
|
||||
topMargin = (0.5f + density * 3f).toInt()
|
||||
}
|
||||
}
|
||||
|
||||
// +ボタン
|
||||
run {
|
||||
val b = ImageButton(activity)
|
||||
val blp = FlexboxLayout.LayoutParams(
|
||||
buttonHeight,
|
||||
buttonHeight
|
||||
).apply {
|
||||
bottomMargin = marginBottom
|
||||
endMargin = marginBetween
|
||||
}
|
||||
b.layoutParams = blp
|
||||
b.background = ContextCompat.getDrawable(
|
||||
activity,
|
||||
R.drawable.btn_bg_transparent_round6dp
|
||||
)
|
||||
|
||||
b.contentDescription = activity.getString(R.string.reaction_add)
|
||||
b.scaleType = ImageView.ScaleType.FIT_CENTER
|
||||
b.padding = paddingV
|
||||
b.setOnClickListener {
|
||||
addReaction(item, null)
|
||||
}
|
||||
|
||||
setIconDrawableId(
|
||||
activity,
|
||||
b,
|
||||
R.drawable.ic_add,
|
||||
color = content_color,
|
||||
alphaMultiplier = 1f
|
||||
)
|
||||
|
||||
box.addView(b)
|
||||
}
|
||||
val reactions = item.reactions?.filter { it.count > 0L }?.notEmpty()
|
||||
if (reactions != null) {
|
||||
|
||||
var lastButton: View? = null
|
||||
|
||||
val options = DecodeOptions(
|
||||
activity,
|
||||
column.access_info,
|
||||
decodeEmoji = true,
|
||||
enlargeEmoji = 1.5f,
|
||||
mentionDefaultHostDomain = column.access_info
|
||||
)
|
||||
|
||||
val actMain = activity
|
||||
val disableEmojiAnimation = Pref.bpDisableEmojiAnimation(actMain.pref)
|
||||
|
||||
for (reaction in reactions) {
|
||||
|
||||
val url = if (disableEmojiAnimation) {
|
||||
reaction.static_url.notEmpty() ?: reaction.url.notEmpty()
|
||||
} else {
|
||||
reaction.url.notEmpty() ?: reaction.static_url.notEmpty()
|
||||
}
|
||||
|
||||
val b = Button(activity).also { btn ->
|
||||
btn.layoutParams = FlexboxLayout.LayoutParams(
|
||||
FlexboxLayout.LayoutParams.WRAP_CONTENT,
|
||||
buttonHeight
|
||||
).apply {
|
||||
endMargin = marginBetween
|
||||
bottomMargin = marginBottom
|
||||
}
|
||||
btn.minWidthCompat = buttonHeight
|
||||
|
||||
btn.allCaps = false
|
||||
btn.tag = reaction
|
||||
|
||||
btn.background = if (reaction.me) {
|
||||
getAdaptiveRippleDrawableRound(
|
||||
actMain,
|
||||
actMain.attrColor(R.attr.colorButtonBgCw),
|
||||
actMain.attrColor(R.attr.colorRippleEffect)
|
||||
)
|
||||
} else {
|
||||
ContextCompat.getDrawable(actMain, R.drawable.btn_bg_transparent_round6dp)
|
||||
}
|
||||
|
||||
btn.setTextColor(content_color)
|
||||
|
||||
btn.setPadding(paddingH, paddingV, paddingH, paddingV)
|
||||
|
||||
|
||||
btn.text = if (url == null) {
|
||||
EmojiDecoder.decodeEmoji(options, "${reaction.name} ${reaction.count}")
|
||||
} else {
|
||||
SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
|
||||
sb.setSpan(
|
||||
NetworkEmojiSpan(url, scale = 1.5f),
|
||||
0,
|
||||
reaction.name.length,
|
||||
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
|
||||
)
|
||||
val invalidator =
|
||||
NetworkEmojiInvalidator(actMain.handler, btn)
|
||||
invalidator.register(sb)
|
||||
extra_invalidator_list.add(invalidator)
|
||||
}
|
||||
}
|
||||
|
||||
btn.setOnClickListener {
|
||||
if (reaction.me) {
|
||||
removeReaction(item, reaction.name)
|
||||
} else {
|
||||
addReaction(item, TootReaction.parseFedibird(jsonObject {
|
||||
put("name", reaction.name)
|
||||
put("count", 1)
|
||||
put("me", true)
|
||||
putNotNull("url", reaction.url)
|
||||
putNotNull("static_url", reaction.static_url)
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
box.addView(b)
|
||||
lastButton = b
|
||||
|
||||
}
|
||||
|
||||
lastButton
|
||||
?.layoutParams
|
||||
?.cast<ViewGroup.MarginLayoutParams>()
|
||||
?.endMargin = 0
|
||||
}
|
||||
|
||||
llAnnouncementExtra.addView(box)
|
||||
}
|
||||
|
||||
|
||||
fun ColumnViewHolder.addReaction(item: TootAnnouncement, sample: TootReaction?) {
|
||||
val column = column ?: return
|
||||
if (sample == null) {
|
||||
EmojiPicker(activity, column.access_info, closeOnSelected = true) { result ->
|
||||
val emoji = result.emoji
|
||||
val code = when (emoji) {
|
||||
is UnicodeEmoji -> emoji.unifiedCode
|
||||
is CustomEmoji -> emoji.shortcode
|
||||
else -> error("unknown emoji type")
|
||||
}
|
||||
ColumnViewHolder.log.d("addReaction: $code ${result.emoji.javaClass.simpleName}")
|
||||
addReaction(item, TootReaction.parseFedibird(jsonObject {
|
||||
put("name", code)
|
||||
put("count", 1)
|
||||
put("me", true)
|
||||
// 以下はカスタム絵文字のみ
|
||||
if (emoji is CustomEmoji) {
|
||||
putNotNull("url", emoji.url)
|
||||
putNotNull("static_url", emoji.static_url)
|
||||
}
|
||||
}))
|
||||
}.show()
|
||||
return
|
||||
}
|
||||
|
||||
TootTaskRunner(activity).run(column.access_info, object : TootTask {
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
return client.request(
|
||||
"/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}",
|
||||
JsonObject().toPutRequestBuilder()
|
||||
)
|
||||
// 200 {}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result ?: return
|
||||
if (result.jsonObject == null) {
|
||||
activity.showToast(true, result.error)
|
||||
} else {
|
||||
sample.count = 0
|
||||
val list = item.reactions
|
||||
if (list == null) {
|
||||
item.reactions = mutableListOf(sample)
|
||||
} else {
|
||||
val reaction = list.find { it.name == sample.name }
|
||||
if (reaction == null) {
|
||||
list.add(sample)
|
||||
} else {
|
||||
reaction.me = true
|
||||
++reaction.count
|
||||
}
|
||||
}
|
||||
column.announcementUpdated = SystemClock.elapsedRealtime()
|
||||
showAnnouncements()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.removeReaction(item: TootAnnouncement, name: String) {
|
||||
val column = column ?: return
|
||||
TootTaskRunner(activity).run(column.access_info, object : TootTask {
|
||||
override suspend fun background(client: TootApiClient): TootApiResult? {
|
||||
return client.request(
|
||||
"/api/v1/announcements/${item.id}/reactions/${name.encodePercent()}",
|
||||
JsonObject().toDeleteRequestBuilder()
|
||||
)
|
||||
// 200 {}
|
||||
}
|
||||
|
||||
override suspend fun handleResult(result: TootApiResult?) {
|
||||
result ?: return
|
||||
if (result.jsonObject == null) {
|
||||
activity.showToast(true, result.error)
|
||||
} else {
|
||||
val it = item.reactions?.iterator() ?: return
|
||||
while (it.hasNext()) {
|
||||
val reaction = it.next()
|
||||
if (reaction.name == name) {
|
||||
reaction.me = false
|
||||
if (--reaction.count <= 0) it.remove()
|
||||
break
|
||||
}
|
||||
}
|
||||
column.announcementUpdated = SystemClock.elapsedRealtime()
|
||||
showAnnouncements()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.ImageView
|
||||
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
|
||||
import jp.juggler.subwaytooter.streaming.canSpeech
|
||||
import jp.juggler.subwaytooter.streaming.canStreaming
|
||||
import jp.juggler.subwaytooter.util.endPadding
|
||||
import jp.juggler.subwaytooter.util.startPadding
|
||||
import jp.juggler.subwaytooter.view.ListDivider
|
||||
import jp.juggler.util.*
|
||||
import kotlinx.coroutines.*
|
||||
import org.jetbrains.anko.backgroundColor
|
||||
import org.jetbrains.anko.bottomPadding
|
||||
import org.jetbrains.anko.topPadding
|
||||
|
||||
fun ColumnViewHolder.closeBitmaps() {
|
||||
try {
|
||||
ivColumnBackgroundImage.visibility = View.GONE
|
||||
ivColumnBackgroundImage.setImageDrawable(null)
|
||||
|
||||
last_image_bitmap?.recycle()
|
||||
last_image_bitmap = null
|
||||
|
||||
last_image_task?.cancel()
|
||||
last_image_task = null
|
||||
|
||||
last_image_uri = null
|
||||
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.trace(ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.loadBackgroundImage(iv: ImageView, url: String?) {
|
||||
try {
|
||||
if (url == null || url.isEmpty() || Pref.bpDontShowColumnBackgroundImage(activity.pref)) {
|
||||
// 指定がないなら閉じる
|
||||
closeBitmaps()
|
||||
return
|
||||
}
|
||||
|
||||
if (url == last_image_uri) {
|
||||
// 今表示してるのと同じ
|
||||
return
|
||||
}
|
||||
|
||||
// 直前の処理をキャンセルする。Bitmapも破棄する
|
||||
closeBitmaps()
|
||||
|
||||
// ロード開始
|
||||
last_image_uri = url
|
||||
val screen_w = iv.resources.displayMetrics.widthPixels
|
||||
val screen_h = iv.resources.displayMetrics.heightPixels
|
||||
|
||||
// 非同期処理を開始
|
||||
last_image_task = GlobalScope.launch(Dispatchers.Main) {
|
||||
val bitmap = try {
|
||||
withContext(Dispatchers.IO) {
|
||||
try {
|
||||
createResizedBitmap(
|
||||
activity, url.toUri(),
|
||||
if (screen_w > screen_h)
|
||||
screen_w
|
||||
else
|
||||
screen_h
|
||||
)
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.trace(ex)
|
||||
null
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
null
|
||||
}
|
||||
if (bitmap != null) {
|
||||
if (!coroutineContext.isActive || url != last_image_uri) {
|
||||
bitmap.recycle()
|
||||
} else {
|
||||
last_image_bitmap = bitmap
|
||||
iv.setImageBitmap(last_image_bitmap)
|
||||
iv.visibility = View.VISIBLE
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.trace(ex)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
fun ColumnViewHolder.onPageDestroy(page_idx: Int) {
|
||||
// タブレットモードの場合、onPageCreateより前に呼ばれる
|
||||
val column = this.column
|
||||
if (column != null) {
|
||||
ColumnViewHolder.log.d("onPageDestroy [%d] %s", page_idx, tvColumnName.text)
|
||||
saveScrollPosition()
|
||||
listView.adapter = null
|
||||
column.removeColumnViewHolder(this)
|
||||
this.column = null
|
||||
}
|
||||
|
||||
closeBitmaps()
|
||||
|
||||
activity.closeListItemPopup()
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.onPageCreate(column: Column, page_idx: Int, page_count: Int) {
|
||||
binding_busy = true
|
||||
try {
|
||||
this.column = column
|
||||
this.page_idx = page_idx
|
||||
|
||||
ColumnViewHolder.log.d("onPageCreate [%d] %s", page_idx, column.getColumnName(true))
|
||||
|
||||
val bSimpleList =
|
||||
column.type != ColumnType.CONVERSATION && Pref.bpSimpleList(activity.pref)
|
||||
|
||||
tvColumnIndex.text = activity.getString(R.string.column_index, page_idx + 1, page_count)
|
||||
tvColumnStatus.text = "?"
|
||||
ivColumnIcon.setImageResource(column.getIconId())
|
||||
|
||||
listView.adapter = null
|
||||
if (listView.itemDecorationCount == 0) {
|
||||
listView.addItemDecoration(ListDivider(activity))
|
||||
}
|
||||
|
||||
val status_adapter = ItemListAdapter(activity, column, this, bSimpleList)
|
||||
this.status_adapter = status_adapter
|
||||
|
||||
val isNotificationColumn = column.isNotificationColumn
|
||||
|
||||
// 添付メディアや正規表現のフィルタ
|
||||
val bAllowFilter = column.canStatusFilter()
|
||||
|
||||
showColumnSetting(false)
|
||||
|
||||
|
||||
|
||||
cbDontCloseColumn.isCheckedNoAnime = column.dont_close
|
||||
cbRemoteOnly.isCheckedNoAnime = column.remote_only
|
||||
cbWithAttachment.isCheckedNoAnime = column.with_attachment
|
||||
cbWithHighlight.isCheckedNoAnime = column.with_highlight
|
||||
cbDontShowBoost.isCheckedNoAnime = column.dont_show_boost
|
||||
cbDontShowFollow.isCheckedNoAnime = column.dont_show_follow
|
||||
cbDontShowFavourite.isCheckedNoAnime = column.dont_show_favourite
|
||||
cbDontShowReply.isCheckedNoAnime = column.dont_show_reply
|
||||
cbDontShowReaction.isCheckedNoAnime = column.dont_show_reaction
|
||||
cbDontShowVote.isCheckedNoAnime = column.dont_show_vote
|
||||
cbDontShowNormalToot.isCheckedNoAnime = column.dont_show_normal_toot
|
||||
cbDontShowNonPublicToot.isCheckedNoAnime = column.dont_show_non_public_toot
|
||||
cbInstanceLocal.isCheckedNoAnime = column.instance_local
|
||||
cbDontStreaming.isCheckedNoAnime = column.dont_streaming
|
||||
cbDontAutoRefresh.isCheckedNoAnime = column.dont_auto_refresh
|
||||
cbHideMediaDefault.isCheckedNoAnime = column.hide_media_default
|
||||
cbSystemNotificationNotRelated.isCheckedNoAnime = column.system_notification_not_related
|
||||
cbEnableSpeech.isCheckedNoAnime = column.enable_speech
|
||||
cbOldApi.isCheckedNoAnime = column.use_old_api
|
||||
|
||||
etRegexFilter.setText(column.regex_text)
|
||||
etSearch.setText(column.search_query)
|
||||
cbResolve.isCheckedNoAnime = column.search_resolve
|
||||
|
||||
cbRemoteOnly.vg(column.canRemoteOnly())
|
||||
|
||||
cbWithAttachment.vg(bAllowFilter)
|
||||
cbWithHighlight.vg(bAllowFilter)
|
||||
etRegexFilter.vg(bAllowFilter)
|
||||
llRegexFilter.vg(bAllowFilter)
|
||||
btnLanguageFilter.vg(bAllowFilter)
|
||||
|
||||
cbDontShowBoost.vg(column.canFilterBoost())
|
||||
cbDontShowReply.vg(column.canFilterReply())
|
||||
cbDontShowNormalToot.vg(column.canFilterNormalToot())
|
||||
cbDontShowNonPublicToot.vg(column.canFilterNonPublicToot())
|
||||
cbDontShowReaction.vg(isNotificationColumn && column.isMisskey)
|
||||
cbDontShowVote.vg(isNotificationColumn)
|
||||
cbDontShowFavourite.vg(isNotificationColumn && !column.isMisskey)
|
||||
cbDontShowFollow.vg(isNotificationColumn)
|
||||
|
||||
cbInstanceLocal.vg(column.type == ColumnType.HASHTAG)
|
||||
|
||||
|
||||
cbDontStreaming.vg(column.canStreaming())
|
||||
cbDontAutoRefresh.vg(column.canAutoRefresh())
|
||||
cbHideMediaDefault.vg(column.canNSFWDefault())
|
||||
cbSystemNotificationNotRelated.vg(column.isNotificationColumn)
|
||||
cbEnableSpeech.vg(column.canSpeech())
|
||||
cbOldApi.vg(column.type == ColumnType.DIRECT_MESSAGES)
|
||||
|
||||
|
||||
btnDeleteNotification.vg(column.isNotificationColumn)
|
||||
|
||||
llSearch.vg(column.isSearchColumn)?.let {
|
||||
btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref))
|
||||
}
|
||||
|
||||
llListList.vg(column.type == ColumnType.LIST_LIST)
|
||||
cbResolve.vg(column.type == ColumnType.SEARCH)
|
||||
|
||||
llHashtagExtra.vg(column.hasHashtagExtra)
|
||||
etHashtagExtraAny.setText(column.hashtag_any)
|
||||
etHashtagExtraAll.setText(column.hashtag_all)
|
||||
etHashtagExtraNone.setText(column.hashtag_none)
|
||||
|
||||
// tvRegexFilterErrorの表示を更新
|
||||
if (bAllowFilter) {
|
||||
isRegexValid()
|
||||
}
|
||||
|
||||
val canRefreshTop = column.canRefreshTopBySwipe()
|
||||
val canRefreshBottom = column.canRefreshBottomBySwipe()
|
||||
|
||||
refreshLayout.isEnabled = canRefreshTop || canRefreshBottom
|
||||
refreshLayout.direction = if (canRefreshTop && canRefreshBottom) {
|
||||
SwipyRefreshLayoutDirection.BOTH
|
||||
} else if (canRefreshTop) {
|
||||
SwipyRefreshLayoutDirection.TOP
|
||||
} else {
|
||||
SwipyRefreshLayoutDirection.BOTTOM
|
||||
}
|
||||
|
||||
bRefreshErrorWillShown = false
|
||||
llRefreshError.clearAnimation()
|
||||
llRefreshError.visibility = View.GONE
|
||||
|
||||
//
|
||||
listView.adapter = status_adapter
|
||||
|
||||
//XXX FastScrollerのサポートを諦める。ライブラリはいくつかあるんだけど、設定でON/OFFできなかったり頭文字バブルを無効にできなかったり
|
||||
// listView.isFastScrollEnabled = ! Pref.bpDisableFastScroller(Pref.pref(activity))
|
||||
|
||||
column.addColumnViewHolder(this)
|
||||
|
||||
lastAnnouncementShown = -1L
|
||||
|
||||
fun dip(dp: Int): Int = (activity.density * dp + 0.5f).toInt()
|
||||
val context = activity
|
||||
|
||||
val announcementsBgColor = Pref.ipAnnouncementsBgColor(App1.pref).notZero()
|
||||
?: context.attrColor(R.attr.colorSearchFormBackground)
|
||||
|
||||
btnAnnouncementsCutout.apply {
|
||||
color = announcementsBgColor
|
||||
}
|
||||
|
||||
llAnnouncementsBox.apply {
|
||||
background = createRoundDrawable(dip(6).toFloat(), announcementsBgColor)
|
||||
val pad_tb = dip(2)
|
||||
setPadding(0, pad_tb, 0, pad_tb)
|
||||
}
|
||||
|
||||
val searchBgColor = Pref.ipSearchBgColor(App1.pref).notZero()
|
||||
?: context.attrColor(R.attr.colorSearchFormBackground)
|
||||
|
||||
llSearch.apply {
|
||||
backgroundColor = searchBgColor
|
||||
startPadding = dip(12)
|
||||
endPadding = dip(12)
|
||||
topPadding = dip(3)
|
||||
bottomPadding = dip(3)
|
||||
}
|
||||
|
||||
llListList.apply {
|
||||
backgroundColor = searchBgColor
|
||||
startPadding = dip(12)
|
||||
endPadding = dip(12)
|
||||
topPadding = dip(3)
|
||||
bottomPadding = dip(3)
|
||||
}
|
||||
|
||||
showColumnColor()
|
||||
|
||||
showContent(reason = "onPageCreate", reset = true)
|
||||
} finally {
|
||||
binding_busy = false
|
||||
}
|
||||
}
|
@ -0,0 +1,280 @@
|
||||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.GestureDetector
|
||||
import android.view.MotionEvent
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import jp.juggler.subwaytooter.util.ScrollPosition
|
||||
import jp.juggler.subwaytooter.view.ListDivider
|
||||
import jp.juggler.util.abs
|
||||
import java.io.Closeable
|
||||
|
||||
private class ErrorFlickListener(
|
||||
private val cvh: ColumnViewHolder,
|
||||
) : View.OnTouchListener, GestureDetector.OnGestureListener {
|
||||
|
||||
private val gd = GestureDetector(cvh.activity, this)
|
||||
val density = cvh.activity.resources.displayMetrics.density
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
override fun onTouch(v: View?, event: MotionEvent?): Boolean {
|
||||
return gd.onTouchEvent(event)
|
||||
}
|
||||
|
||||
override fun onShowPress(e: MotionEvent?) {
|
||||
}
|
||||
|
||||
override fun onLongPress(e: MotionEvent?) {
|
||||
}
|
||||
|
||||
override fun onSingleTapUp(e: MotionEvent?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDown(e: MotionEvent?): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onScroll(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
distanceX: Float,
|
||||
distanceY: Float
|
||||
): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onFling(
|
||||
e1: MotionEvent?,
|
||||
e2: MotionEvent?,
|
||||
velocityX: Float,
|
||||
velocityY: Float
|
||||
): Boolean {
|
||||
|
||||
val vx = velocityX.abs()
|
||||
val vy = velocityY.abs()
|
||||
if (vy < vx * 1.5f) {
|
||||
// フリック方向が上下ではない
|
||||
ColumnViewHolder.log.d("fling? not vertical view. $vx $vy")
|
||||
} else {
|
||||
|
||||
val vyDp = vy / density
|
||||
val limit = 1024f
|
||||
ColumnViewHolder.log.d("fling? $vyDp/$limit")
|
||||
if (vyDp >= limit) {
|
||||
val column = cvh.column
|
||||
if (column != null && column.lastTask == null) {
|
||||
column.startLoading()
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
private class AdapterItemHeightWorkarea(
|
||||
val listView: RecyclerView,
|
||||
val adapter: ItemListAdapter
|
||||
) : Closeable {
|
||||
|
||||
private val item_width: Int
|
||||
private val widthSpec: Int
|
||||
var lastViewType: Int = -1
|
||||
var lastViewHolder: RecyclerView.ViewHolder? = null
|
||||
|
||||
init {
|
||||
this.item_width = listView.width - listView.paddingLeft - listView.paddingRight
|
||||
this.widthSpec = View.MeasureSpec.makeMeasureSpec(item_width, View.MeasureSpec.EXACTLY)
|
||||
}
|
||||
|
||||
override fun close() {
|
||||
val childViewHolder = lastViewHolder
|
||||
if (childViewHolder != null) {
|
||||
adapter.onViewRecycled(childViewHolder)
|
||||
lastViewHolder = null
|
||||
}
|
||||
}
|
||||
|
||||
// この関数はAdapterViewの項目の(marginを含む)高さを返す
|
||||
fun getAdapterItemHeight(adapterIndex: Int): Int {
|
||||
|
||||
fun View.getTotalHeight(): Int {
|
||||
measure(widthSpec, ColumnViewHolder.heightSpec)
|
||||
val lp = layoutParams as? ViewGroup.MarginLayoutParams
|
||||
return measuredHeight + (lp?.topMargin ?: 0) + (lp?.bottomMargin ?: 0)
|
||||
}
|
||||
|
||||
listView.findViewHolderForAdapterPosition(adapterIndex)?.itemView?.let {
|
||||
return it.getTotalHeight()
|
||||
}
|
||||
|
||||
ColumnViewHolder.log.d("getAdapterItemHeight idx=$adapterIndex createView")
|
||||
|
||||
val viewType = adapter.getItemViewType(adapterIndex)
|
||||
|
||||
var childViewHolder = lastViewHolder
|
||||
if (childViewHolder == null || lastViewType != viewType) {
|
||||
if (childViewHolder != null) {
|
||||
adapter.onViewRecycled(childViewHolder)
|
||||
}
|
||||
childViewHolder = adapter.onCreateViewHolder(listView, viewType)
|
||||
lastViewHolder = childViewHolder
|
||||
lastViewType = viewType
|
||||
}
|
||||
adapter.onBindViewHolder(childViewHolder, adapterIndex)
|
||||
return childViewHolder.itemView.getTotalHeight()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun ColumnViewHolder.initLoadingTextView() {
|
||||
llLoading.setOnTouchListener(ErrorFlickListener(this))
|
||||
}
|
||||
|
||||
// 特定の要素が特定の位置に来るようにスクロール位置を調整する
|
||||
fun ColumnViewHolder.setListItemTop(listIndex: Int, yArg: Int) {
|
||||
var adapterIndex = column?.toAdapterIndex(listIndex) ?: return
|
||||
|
||||
val adapter = status_adapter
|
||||
if (adapter == null) {
|
||||
ColumnViewHolder.log.e("setListItemTop: missing status adapter")
|
||||
return
|
||||
}
|
||||
|
||||
var y = yArg
|
||||
AdapterItemHeightWorkarea(listView, adapter).use { workarea ->
|
||||
while (y > 0 && adapterIndex > 0) {
|
||||
--adapterIndex
|
||||
y -= workarea.getAdapterItemHeight(adapterIndex)
|
||||
y -= ListDivider.height
|
||||
}
|
||||
}
|
||||
|
||||
if (adapterIndex == 0 && y > 0) y = 0
|
||||
listLayoutManager.scrollToPositionWithOffset(adapterIndex, y)
|
||||
}
|
||||
|
||||
// この関数は scrollToPositionWithOffset 用のオフセットを返す
|
||||
fun ColumnViewHolder.getListItemOffset(listIndex: Int): Int {
|
||||
|
||||
val adapterIndex = column?.toAdapterIndex(listIndex)
|
||||
?: return 0
|
||||
|
||||
val childView = listLayoutManager.findViewByPosition(adapterIndex)
|
||||
?: throw IndexOutOfBoundsException("findViewByPosition($adapterIndex) returns null.")
|
||||
|
||||
// スクロールとともにtopは減少する
|
||||
// しかしtopMarginがあるので最大値は4である
|
||||
// この関数は scrollToPositionWithOffset 用のオフセットを返すので top - topMargin を返す
|
||||
return childView.top - ((childView.layoutParams as? ViewGroup.MarginLayoutParams)?.topMargin
|
||||
?: 0)
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.findFirstVisibleListItem(): Int {
|
||||
|
||||
val adapterIndex = listLayoutManager.findFirstVisibleItemPosition()
|
||||
|
||||
if (adapterIndex == RecyclerView.NO_POSITION)
|
||||
throw IndexOutOfBoundsException()
|
||||
|
||||
return column?.toListIndex(adapterIndex)
|
||||
?: throw IndexOutOfBoundsException()
|
||||
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.scrollToTop() {
|
||||
try {
|
||||
listView.stopScroll()
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.e(ex, "stopScroll failed.")
|
||||
}
|
||||
try {
|
||||
listLayoutManager.scrollToPositionWithOffset(0, 0)
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.e(ex, "scrollToPositionWithOffset failed.")
|
||||
}
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.scrollToTop2() {
|
||||
val status_adapter = this.status_adapter
|
||||
if (binding_busy || status_adapter == null) return
|
||||
if (status_adapter.itemCount > 0) {
|
||||
scrollToTop()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ColumnViewHolder.saveScrollPosition(): Boolean {
|
||||
val column = this.column
|
||||
when {
|
||||
column == null -> ColumnViewHolder.log.d("saveScrollPosition [%d] , column==null", page_idx)
|
||||
|
||||
column.is_dispose.get() -> ColumnViewHolder.log.d(
|
||||
"saveScrollPosition [%d] , column is disposed",
|
||||
page_idx
|
||||
)
|
||||
|
||||
listView.visibility != View.VISIBLE -> {
|
||||
val scroll_save = ScrollPosition()
|
||||
column.scroll_save = scroll_save
|
||||
ColumnViewHolder.log.d(
|
||||
"saveScrollPosition [%d] %s , listView is not visible, save %s,%s",
|
||||
page_idx,
|
||||
column.getColumnName(true),
|
||||
scroll_save.adapterIndex,
|
||||
scroll_save.offset
|
||||
)
|
||||
return true
|
||||
}
|
||||
|
||||
else -> {
|
||||
val scroll_save = ScrollPosition(this)
|
||||
column.scroll_save = scroll_save
|
||||
ColumnViewHolder.log.d(
|
||||
"saveScrollPosition [%d] %s , listView is visible, save %s,%s",
|
||||
page_idx,
|
||||
column.getColumnName(true),
|
||||
scroll_save.adapterIndex,
|
||||
scroll_save.offset
|
||||
)
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.setScrollPosition(sp: ScrollPosition, deltaDp: Float = 0f) {
|
||||
val last_adapter = listView.adapter
|
||||
if (column == null || last_adapter == null) return
|
||||
|
||||
sp.restore(this)
|
||||
|
||||
// 復元した後に意図的に少し上下にずらしたい
|
||||
val dy = (deltaDp * activity.density + 0.5f).toInt()
|
||||
if (dy != 0) listView.postDelayed(Runnable {
|
||||
if (column == null || listView.adapter !== last_adapter) return@Runnable
|
||||
|
||||
try {
|
||||
val recycler = ColumnViewHolder.fieldRecycler.get(listView) as RecyclerView.Recycler
|
||||
val state = ColumnViewHolder.fieldState.get(listView) as RecyclerView.State
|
||||
listLayoutManager.scrollVerticallyBy(dy, recycler, state)
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.trace(ex)
|
||||
ColumnViewHolder.log.e("can't access field in class %s", RecyclerView::class.java.simpleName)
|
||||
}
|
||||
}, 20L)
|
||||
}
|
||||
|
||||
|
||||
// 相対時刻を更新する
|
||||
fun ColumnViewHolder.updateRelativeTime() = rebindAdapterItems()
|
||||
|
||||
fun ColumnViewHolder.rebindAdapterItems() {
|
||||
for (childIndex in 0 until listView.childCount) {
|
||||
val adapterIndex = listView.getChildAdapterPosition(listView.getChildAt(childIndex))
|
||||
if (adapterIndex == RecyclerView.NO_POSITION) continue
|
||||
status_adapter?.notifyItemChanged(adapterIndex)
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.graphics.Color
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import android.widget.TextView
|
||||
import jp.juggler.util.applyAlphaMultiplier
|
||||
import jp.juggler.util.attrColor
|
||||
import jp.juggler.util.getAdaptiveRippleDrawableRound
|
||||
import jp.juggler.util.vg
|
||||
import org.jetbrains.anko.backgroundDrawable
|
||||
import org.jetbrains.anko.textColor
|
||||
|
||||
fun ColumnViewHolder.clickQuickFilter(filter: Int) {
|
||||
column?.quick_filter = filter
|
||||
showQuickFilter()
|
||||
activity.app_state.saveColumnList()
|
||||
column?.startLoading()
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showQuickFilter() {
|
||||
val column = this.column ?: return
|
||||
|
||||
svQuickFilter.vg(column.isNotificationColumn) ?: return
|
||||
|
||||
btnQuickFilterReaction.vg(column.isMisskey)
|
||||
btnQuickFilterFavourite.vg(!column.isMisskey)
|
||||
|
||||
val insideColumnSetting = Pref.bpMoveNotificationsQuickFilter(activity.pref)
|
||||
|
||||
val showQuickFilterButton: (btn: View, iconId: Int, selected: Boolean) -> Unit
|
||||
|
||||
if (insideColumnSetting) {
|
||||
svQuickFilter.setBackgroundColor(0)
|
||||
|
||||
val colorFg = activity.attrColor(R.attr.colorContentText)
|
||||
val colorBgSelected = colorFg.applyAlphaMultiplier(0.25f)
|
||||
val colorFgList = ColorStateList.valueOf(colorFg)
|
||||
val colorBg = activity.attrColor(R.attr.colorColumnSettingBackground)
|
||||
showQuickFilterButton = { btn, iconId, selected ->
|
||||
btn.backgroundDrawable =
|
||||
getAdaptiveRippleDrawableRound(
|
||||
activity,
|
||||
if (selected) colorBgSelected else colorBg,
|
||||
colorFg,
|
||||
roundNormal = true
|
||||
)
|
||||
|
||||
when (btn) {
|
||||
is TextView -> btn.textColor = colorFg
|
||||
|
||||
is ImageButton -> {
|
||||
btn.setImageResource(iconId)
|
||||
btn.imageTintList = colorFgList
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val colorBg = column.getHeaderBackgroundColor()
|
||||
val colorFg = column.getHeaderNameColor()
|
||||
val colorFgList = ColorStateList.valueOf(colorFg)
|
||||
val colorBgSelected = Color.rgb(
|
||||
(Color.red(colorBg) * 3 + Color.red(colorFg)) / 4,
|
||||
(Color.green(colorBg) * 3 + Color.green(colorFg)) / 4,
|
||||
(Color.blue(colorBg) * 3 + Color.blue(colorFg)) / 4
|
||||
)
|
||||
svQuickFilter.setBackgroundColor(colorBg)
|
||||
|
||||
showQuickFilterButton = { btn, iconId, selected ->
|
||||
|
||||
btn.backgroundDrawable = getAdaptiveRippleDrawableRound(
|
||||
activity,
|
||||
if (selected) colorBgSelected else colorBg,
|
||||
colorFg
|
||||
)
|
||||
|
||||
when (btn) {
|
||||
is TextView -> btn.textColor = colorFg
|
||||
|
||||
is ImageButton -> {
|
||||
btn.setImageResource(iconId)
|
||||
btn.imageTintList = colorFgList
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterAll,
|
||||
0,
|
||||
column.quick_filter == Column.QUICK_FILTER_ALL
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterMention,
|
||||
R.drawable.ic_reply,
|
||||
column.quick_filter == Column.QUICK_FILTER_MENTION
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterFavourite,
|
||||
R.drawable.ic_star,
|
||||
column.quick_filter == Column.QUICK_FILTER_FAVOURITE
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterBoost,
|
||||
R.drawable.ic_repeat,
|
||||
column.quick_filter == Column.QUICK_FILTER_BOOST
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterFollow,
|
||||
R.drawable.ic_follow_plus,
|
||||
column.quick_filter == Column.QUICK_FILTER_FOLLOW
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterPost,
|
||||
R.drawable.ic_send,
|
||||
column.quick_filter == Column.QUICK_FILTER_POST
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterReaction,
|
||||
R.drawable.ic_add,
|
||||
column.quick_filter == Column.QUICK_FILTER_REACTION
|
||||
)
|
||||
|
||||
showQuickFilterButton(
|
||||
btnQuickFilterVote,
|
||||
R.drawable.ic_vote,
|
||||
column.quick_filter == Column.QUICK_FILTER_VOTE
|
||||
)
|
||||
}
|
@ -0,0 +1,215 @@
|
||||
package jp.juggler.subwaytooter
|
||||
|
||||
import android.content.res.ColorStateList
|
||||
import android.text.TextUtils
|
||||
import android.view.View
|
||||
import android.view.animation.AlphaAnimation
|
||||
import android.view.animation.Animation
|
||||
import jp.juggler.util.notZero
|
||||
import jp.juggler.util.vg
|
||||
import org.jetbrains.anko.textColor
|
||||
|
||||
// カラムヘッダなど、負荷が低い部分の表示更新
|
||||
fun ColumnViewHolder.showColumnHeader() {
|
||||
activity.handler.removeCallbacks(procShowColumnHeader)
|
||||
activity.handler.postDelayed(procShowColumnHeader, 50L)
|
||||
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showColumnStatus() {
|
||||
activity.handler.removeCallbacks(procShowColumnStatus)
|
||||
activity.handler.postDelayed(procShowColumnStatus, 50L)
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showColumnColor() {
|
||||
val column = this.column
|
||||
if (column == null || column.is_dispose.get()) return
|
||||
|
||||
// カラムヘッダ背景
|
||||
column.setHeaderBackground(llColumnHeader)
|
||||
|
||||
// カラムヘッダ文字色(A)
|
||||
var c = column.getHeaderNameColor()
|
||||
val csl = ColorStateList.valueOf(c)
|
||||
tvColumnName.textColor = c
|
||||
ivColumnIcon.imageTintList = csl
|
||||
btnAnnouncements.imageTintList = csl
|
||||
btnColumnSetting.imageTintList = csl
|
||||
btnColumnReload.imageTintList = csl
|
||||
btnColumnClose.imageTintList = csl
|
||||
|
||||
// カラムヘッダ文字色(B)
|
||||
c = column.getHeaderPageNumberColor()
|
||||
tvColumnIndex.textColor = c
|
||||
tvColumnStatus.textColor = c
|
||||
|
||||
// カラム内部の背景色
|
||||
flColumnBackground.setBackgroundColor(
|
||||
column.column_bg_color.notZero()
|
||||
?: Column.defaultColorContentBg
|
||||
)
|
||||
|
||||
// カラム内部の背景画像
|
||||
ivColumnBackgroundImage.alpha = column.column_bg_image_alpha
|
||||
loadBackgroundImage(ivColumnBackgroundImage, column.column_bg_image)
|
||||
|
||||
// エラー表示
|
||||
tvLoading.textColor = column.getContentColor()
|
||||
|
||||
status_adapter?.findHeaderViewHolder(listView)?.showColor()
|
||||
|
||||
// カラム色を変更したらクイックフィルタの色も変わる場合がある
|
||||
showQuickFilter()
|
||||
|
||||
showAnnouncements(force = false)
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showError(message: String) {
|
||||
hideRefreshError()
|
||||
|
||||
refreshLayout.isRefreshing = false
|
||||
refreshLayout.visibility = View.GONE
|
||||
|
||||
llLoading.visibility = View.VISIBLE
|
||||
tvLoading.text = message
|
||||
btnConfirmMail.vg(column?.access_info?.isConfirmed == false)
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showColumnCloseButton() {
|
||||
val dont_close = column?.dont_close ?: return
|
||||
btnColumnClose.isEnabled = !dont_close
|
||||
btnColumnClose.alpha = if (dont_close) 0.3f else 1f
|
||||
}
|
||||
|
||||
internal fun ColumnViewHolder.showContent(
|
||||
reason: String,
|
||||
changeList: List<AdapterChange>? = null,
|
||||
reset: Boolean = false
|
||||
) {
|
||||
// クラッシュレポートにadapterとリストデータの状態不整合が多かったので、
|
||||
// とりあえずリストデータ変更の通知だけは最優先で行っておく
|
||||
try {
|
||||
status_adapter?.notifyChange(reason, changeList, reset)
|
||||
} catch (ex: Throwable) {
|
||||
ColumnViewHolder.log.trace(ex)
|
||||
}
|
||||
|
||||
showColumnHeader()
|
||||
showColumnStatus()
|
||||
|
||||
val column = this.column
|
||||
if (column == null || column.is_dispose.get()) {
|
||||
showError("column was disposed.")
|
||||
return
|
||||
}
|
||||
|
||||
if (!column.bFirstInitialized) {
|
||||
showError("initializing")
|
||||
return
|
||||
}
|
||||
|
||||
if (column.bInitialLoading) {
|
||||
var message: String? = column.task_progress
|
||||
if (message == null) message = "loading?"
|
||||
showError(message)
|
||||
return
|
||||
}
|
||||
|
||||
val initialLoadingError = column.mInitialLoadingError
|
||||
if (initialLoadingError.isNotEmpty()) {
|
||||
showError(initialLoadingError)
|
||||
return
|
||||
}
|
||||
|
||||
val status_adapter = this.status_adapter
|
||||
|
||||
if (status_adapter == null || status_adapter.itemCount == 0) {
|
||||
showError(activity.getString(R.string.list_empty))
|
||||
return
|
||||
}
|
||||
|
||||
llLoading.visibility = View.GONE
|
||||
|
||||
refreshLayout.visibility = View.VISIBLE
|
||||
|
||||
status_adapter.findHeaderViewHolder(listView)?.bindData(column)
|
||||
|
||||
if (column.bRefreshLoading) {
|
||||
hideRefreshError()
|
||||
} else {
|
||||
refreshLayout.isRefreshing = false
|
||||
showRefreshError()
|
||||
}
|
||||
proc_restoreScrollPosition.run()
|
||||
|
||||
}
|
||||
|
||||
fun ColumnViewHolder.showColumnSetting(show: Boolean): Boolean {
|
||||
llColumnSetting.vg(show)
|
||||
llColumnHeader.invalidate()
|
||||
return show
|
||||
}
|
||||
|
||||
|
||||
fun ColumnViewHolder.showRefreshError() {
|
||||
val column = column
|
||||
if (column == null) {
|
||||
hideRefreshError()
|
||||
return
|
||||
}
|
||||
|
||||
val refreshError = column.mRefreshLoadingError
|
||||
// val refreshErrorTime = column.mRefreshLoadingErrorTime
|
||||
if (refreshError.isEmpty()) {
|
||||
hideRefreshError()
|
||||
return
|
||||
}
|
||||
|
||||
tvRefreshError.text = refreshError
|
||||
when (column.mRefreshLoadingErrorPopupState) {
|
||||
// initially expanded
|
||||
0 -> {
|
||||
tvRefreshError.isSingleLine = false
|
||||
tvRefreshError.ellipsize = null
|
||||
}
|
||||
|
||||
// tap to minimize
|
||||
1 -> {
|
||||
tvRefreshError.isSingleLine = true
|
||||
tvRefreshError.ellipsize = TextUtils.TruncateAt.END
|
||||
}
|
||||
}
|
||||
|
||||
if (!bRefreshErrorWillShown) {
|
||||
bRefreshErrorWillShown = true
|
||||
if (llRefreshError.visibility == View.GONE) {
|
||||
llRefreshError.visibility = View.VISIBLE
|
||||
val aa = AlphaAnimation(0f, 1f)
|
||||
aa.duration = 666L
|
||||
llRefreshError.clearAnimation()
|
||||
llRefreshError.startAnimation(aa)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fun ColumnViewHolder.hideRefreshError() {
|
||||
if (!bRefreshErrorWillShown) return
|
||||
bRefreshErrorWillShown = false
|
||||
if (llRefreshError.visibility == View.GONE) return
|
||||
val aa = AlphaAnimation(1f, 0f)
|
||||
aa.duration = 666L
|
||||
aa.setAnimationListener(object : Animation.AnimationListener {
|
||||
override fun onAnimationRepeat(animation: Animation?) {
|
||||
}
|
||||
|
||||
override fun onAnimationStart(animation: Animation?) {
|
||||
}
|
||||
|
||||
override fun onAnimationEnd(animation: Animation?) {
|
||||
if (!bRefreshErrorWillShown) llRefreshError.visibility = View.GONE
|
||||
}
|
||||
})
|
||||
llRefreshError.clearAnimation()
|
||||
llRefreshError.startAnimation(aa)
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user