SubwayTooter-Android-App/app/src/main/java/jp/juggler/subwaytooter/actmain/SideMenuAdapter.kt

528 lines
20 KiB
Kotlin

package jp.juggler.subwaytooter.actmain
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.drawable.StateListDrawable
import android.os.Handler
import android.text.Spannable
import android.text.SpannableStringBuilder
import android.text.TextPaint
import android.text.method.LinkMovementMethod
import android.text.style.ClickableSpan
import android.text.style.ForegroundColorSpan
import android.view.View
import android.view.ViewGroup
import android.widget.BaseAdapter
import android.widget.FrameLayout
import android.widget.ListView
import android.widget.TextView
import androidx.core.view.GravityCompat
import androidx.drawerlayout.widget.DrawerLayout
import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.action.*
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.column.ColumnType
import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.pref.PrefB
import jp.juggler.subwaytooter.pref.PrefS
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.VersionString
import jp.juggler.subwaytooter.util.openBrowser
import jp.juggler.util.*
import kotlinx.coroutines.*
import org.jetbrains.anko.backgroundColor
import java.lang.ref.WeakReference
import java.util.*
import java.util.concurrent.TimeUnit
import kotlin.math.abs
class SideMenuAdapter(
private val actMain: ActMain,
val handler: Handler,
navigationView: ViewGroup,
private val drawer: DrawerLayout,
) : BaseAdapter() {
companion object {
private val log = LogCategory("SideMenuAdapter")
private val itemTypeCount = ItemType.values().size
private var lastVersionView: WeakReference<TextView>? = null
private var versionRow = SpannableStringBuilder("")
private var releaseInfo: JsonObject? = null
private fun clickableSpan(url: String) =
object : ClickableSpan() {
override fun onClick(widget: View) {
widget.activity?.openBrowser(url)
}
override fun updateDrawState(ds: TextPaint) {
super.updateDrawState(ds)
ds.isUnderlineText = false
}
}
// 文字列を組み立ててhandler経由でViewに設定する
// メインスレッドでもそれ以外でも動作する
fun afterGet(appContext: Context, handler: Handler, currentVersion: String) {
versionRow = SpannableStringBuilder().apply {
append(
appContext.getString(
R.string.app_name_with_version,
appContext.getString(R.string.app_name),
currentVersion
)
)
val newRelease = releaseInfo?.jsonObject(
if (PrefB.bpCheckBetaVersion()) "beta" else "stable"
)
val newVersion =
(newRelease?.string("name")?.notEmpty() ?: newRelease?.string("tag_name"))
?.replace("""(v|version)\s*""".toRegex(RegexOption.IGNORE_CASE), "")
?.trim()
if (newVersion == null || newVersion.isEmpty() || VersionString(currentVersion) >= VersionString(
newVersion
)
) {
val url = "https://github.com/tateisu/SubwayTooter/releases"
append("\n")
val start = length
append(appContext.getString(R.string.release_note))
setSpan(
clickableSpan(url),
start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
} else {
append("\n")
var start = length
append(
appContext.getString(
R.string.new_version_available,
newVersion
)
)
setSpan(
ForegroundColorSpan(
appContext.attrColor(R.attr.colorRegexFilterError)
),
start, length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
newRelease?.string("html_url")?.let { url ->
append("\n")
start = length
append(appContext.getString(R.string.release_note_with_assets))
setSpan(
clickableSpan(url),
start, length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
}
}
}
handler.post { lastVersionView?.get()?.text = versionRow }
}
// メインスレッドから呼ばれる
private fun checkVersion(appContext: Context, handler: Handler) {
val currentVersion = try {
appContext.packageManager.getPackageInfo(appContext.packageName, 0).versionName
} catch (ignored: PackageManager.NameNotFoundException) {
"??"
}
versionRow = SpannableStringBuilder().apply {
append(
appContext.getString(
R.string.app_name_with_version,
appContext.getString(R.string.app_name),
currentVersion
)
)
}
val lastUpdated = releaseInfo?.string("updated_at")?.let { TootStatus.parseTime(it) }
if (lastUpdated != null && System.currentTimeMillis() - lastUpdated < 86400000L) {
afterGet(appContext, handler, currentVersion)
} else {
launchIO {
val json =
App1.getHttpCached("https://mastodon-msg.juggler.jp/appVersion/appVersion.json")
?.decodeUTF8()?.decodeJsonObject()
if (json != null) {
releaseInfo = json
afterGet(appContext, handler, currentVersion)
}
}
}
}
}
private enum class ItemType(val id: Int) {
IT_NORMAL(0),
IT_GROUP_HEADER(1),
IT_DIVIDER(2),
IT_VERSION(3),
IT_TIMEZONE(4)
}
private class Item(
// 項目の文字列リソース or 0: divider, 1: バージョン表記, 2: タイムゾーン
val title: Int = 0,
val icon: Int = 0,
val action: ActMain.() -> Unit = {},
) {
val itemType: ItemType
get() = when {
title == 0 -> ItemType.IT_DIVIDER
title == 1 -> ItemType.IT_VERSION
title == 2 -> ItemType.IT_TIMEZONE
icon == 0 -> ItemType.IT_GROUP_HEADER
else -> ItemType.IT_NORMAL
}
}
/*
no title => section divider
else no icon => section header with title
else => menu item with icon and title
*/
private val list = arrayOf(
Item(icon = R.drawable.ic_info, title = 1),
Item(icon = R.drawable.ic_info, title = 2),
Item(),
Item(title = R.string.account),
Item(title = R.string.account_add, icon = R.drawable.ic_account_add) {
accountAdd()
},
Item(icon = R.drawable.ic_settings, title = R.string.account_setting) {
accountOpenSetting()
},
Item(),
Item(title = R.string.column),
Item(icon = R.drawable.ic_list_numbered, title = R.string.column_list) {
openColumnList()
},
Item(icon = R.drawable.ic_close, title = R.string.close_all_columns) {
closeColumnAll()
},
Item(icon = R.drawable.ic_paste, title = R.string.open_column_from_url) {
openColumnFromUrl()
},
Item(icon = R.drawable.ic_home, title = R.string.home) {
timeline(defaultInsertPosition, ColumnType.HOME)
},
Item(icon = R.drawable.ic_announcement, title = R.string.notifications) {
timeline(defaultInsertPosition, ColumnType.NOTIFICATIONS)
},
Item(icon = R.drawable.ic_mail, title = R.string.direct_messages) {
timeline(defaultInsertPosition, ColumnType.DIRECT_MESSAGES)
},
Item(icon = R.drawable.ic_share, title = R.string.misskey_hybrid_timeline_long) {
timeline(defaultInsertPosition, ColumnType.MISSKEY_HYBRID)
},
Item(icon = R.drawable.ic_run, title = R.string.local_timeline) {
timeline(defaultInsertPosition, ColumnType.LOCAL)
},
Item(icon = R.drawable.ic_bike, title = R.string.federate_timeline) {
timeline(defaultInsertPosition, ColumnType.FEDERATE)
},
Item(icon = R.drawable.ic_list_list, title = R.string.lists) {
timeline(defaultInsertPosition, ColumnType.LIST_LIST)
},
Item(icon = R.drawable.ic_satellite, title = R.string.antenna_list_misskey) {
timeline(defaultInsertPosition, ColumnType.MISSKEY_ANTENNA_LIST)
},
Item(icon = R.drawable.ic_search, title = R.string.search) {
timeline(defaultInsertPosition, ColumnType.SEARCH, args = arrayOf("", false))
},
Item(icon = R.drawable.ic_trend, title = R.string.trend_tag) {
timeline(defaultInsertPosition, ColumnType.TREND_TAG)
},
Item(icon = R.drawable.ic_trend, title = R.string.trend_link) {
timeline(defaultInsertPosition, ColumnType.TREND_LINK)
},
Item(icon = R.drawable.ic_trend, title = R.string.trend_post) {
timeline(defaultInsertPosition, ColumnType.TREND_POST)
},
Item(icon = R.drawable.ic_star, title = R.string.favourites) {
timeline(defaultInsertPosition, ColumnType.FAVOURITES)
},
Item(icon = R.drawable.ic_bookmark, title = R.string.bookmarks) {
timeline(defaultInsertPosition, ColumnType.BOOKMARKS)
},
Item(icon = R.drawable.ic_face, title = R.string.reactioned_posts) {
launchMain {
accountListCanSeeMyReactions()?.let { list ->
if (list.isEmpty()) {
showToast(false, R.string.not_available_for_current_accounts)
} else {
val columnType = ColumnType.REACTIONS
pickAccount(
accountListArg = list.toMutableList(),
bAuto = true,
message = getString(
R.string.account_picker_add_timeline_of,
columnType.name1(applicationContext)
)
)?.let { addColumn(defaultInsertPosition, it, columnType) }
}
}
}
},
Item(icon = R.drawable.ic_account_box, title = R.string.profile) {
timeline(defaultInsertPosition, ColumnType.PROFILE)
},
Item(icon = R.drawable.ic_follow_wait, title = R.string.follow_requests) {
timeline(defaultInsertPosition, ColumnType.FOLLOW_REQUESTS)
},
Item(icon = R.drawable.ic_follow_plus, title = R.string.follow_suggestion) {
timeline(defaultInsertPosition, ColumnType.FOLLOW_SUGGESTION)
},
Item(icon = R.drawable.ic_follow_plus, title = R.string.endorse_set) {
timeline(defaultInsertPosition, ColumnType.ENDORSEMENT)
},
Item(icon = R.drawable.ic_follow_plus, title = R.string.profile_directory) {
serverProfileDirectoryFromSideMenu()
},
Item(icon = R.drawable.ic_volume_off, title = R.string.muted_users) {
timeline(defaultInsertPosition, ColumnType.MUTES)
},
Item(icon = R.drawable.ic_block, title = R.string.blocked_users) {
timeline(defaultInsertPosition, ColumnType.BLOCKS)
},
Item(icon = R.drawable.ic_volume_off, title = R.string.keyword_filters) {
timeline(defaultInsertPosition, ColumnType.KEYWORD_FILTER)
},
Item(icon = R.drawable.ic_cloud_off, title = R.string.blocked_domains) {
timeline(defaultInsertPosition, ColumnType.DOMAIN_BLOCKS)
},
Item(icon = R.drawable.ic_timer, title = R.string.scheduled_status_list) {
timeline(defaultInsertPosition, ColumnType.SCHEDULED_STATUS)
},
Item(),
Item(title = R.string.toot_search),
Item(icon = R.drawable.ic_search, title = R.string.mastodon_search_portal) {
addColumn(defaultInsertPosition, SavedAccount.na, ColumnType.SEARCH_MSP, "")
},
Item(icon = R.drawable.ic_search, title = R.string.tootsearch) {
addColumn(defaultInsertPosition, SavedAccount.na, ColumnType.SEARCH_TS, "")
},
Item(icon = R.drawable.ic_search, title = R.string.notestock) {
addColumn(defaultInsertPosition, SavedAccount.na, ColumnType.SEARCH_NOTESTOCK, "")
},
Item(),
Item(title = R.string.setting),
Item(icon = R.drawable.ic_settings, title = R.string.app_setting) {
arAppSetting.launch(
ActAppSetting.createIntent(this)
)
},
Item(icon = R.drawable.ic_settings, title = R.string.highlight_word) {
startActivity(Intent(this, ActHighlightWordList::class.java))
},
Item(icon = R.drawable.ic_volume_off, title = R.string.muted_app) {
startActivity(Intent(this, ActMutedApp::class.java))
},
Item(icon = R.drawable.ic_volume_off, title = R.string.muted_word) {
startActivity(Intent(this, ActMutedWord::class.java))
},
Item(icon = R.drawable.ic_volume_off, title = R.string.fav_muted_user) {
startActivity(Intent(this, ActFavMute::class.java))
},
Item(
icon = R.drawable.ic_volume_off,
title = R.string.muted_users_from_pseudo_account
) {
startActivity(Intent(this, ActMutedPseudoAccount::class.java))
},
Item(icon = R.drawable.ic_info, title = R.string.app_about) {
arAbout.launch(
Intent(this, ActAbout::class.java)
)
},
Item(icon = R.drawable.ic_info, title = R.string.oss_license) {
startActivity(Intent(this, ActOSSLicense::class.java))
},
Item(icon = R.drawable.ic_hot_tub, title = R.string.app_exit) {
finish()
}
)
private val iconColor = actMain.attrColor(R.attr.colorTimeSmall)
override fun getCount(): Int = list.size
override fun getItem(position: Int): Any = list[position]
override fun getItemId(position: Int): Long = 0L
override fun getViewTypeCount(): Int = itemTypeCount
override fun getItemViewType(position: Int): Int = list[position].itemType.id
private inline fun <reified T : View> viewOrInflate(
view: View?,
parent: ViewGroup?,
resId: Int,
): T =
(view ?: actMain.layoutInflater.inflate(resId, parent, false))
as? T ?: error("invalid view type! ${T::class.java.simpleName}")
override fun getView(position: Int, view: View?, parent: ViewGroup?): View =
list[position].run {
when (itemType) {
ItemType.IT_DIVIDER ->
viewOrInflate(view, parent, R.layout.lv_sidemenu_separator)
ItemType.IT_GROUP_HEADER ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_group).apply {
text = actMain.getString(title)
}
ItemType.IT_NORMAL ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
isAllCaps = false
text = actMain.getString(title)
val drawable = createColoredDrawable(actMain, icon, iconColor, 1f)
setCompoundDrawablesRelativeWithIntrinsicBounds(
drawable,
null,
null,
null
)
setOnClickListener {
action(actMain)
drawer.closeDrawer(GravityCompat.START)
}
}
ItemType.IT_VERSION ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
lastVersionView = WeakReference(this)
movementMethod = LinkMovementMethod.getInstance()
textSize = 18f
isAllCaps = false
background = null
text = versionRow
}
ItemType.IT_TIMEZONE ->
viewOrInflate<TextView>(view, parent, R.layout.lv_sidemenu_item).apply {
textSize = 14f
isAllCaps = false
background = null
text = getTimeZoneString(context)
}
}
}
private fun getTimeZoneString(context: Context): String {
try {
var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone()
if (tzId.isBlank()) {
return tz.displayName + "(" + context.getString(R.string.device_timezone) + ")"
}
tz = TimeZone.getTimeZone(tzId)
var offset = tz.rawOffset.toLong()
return when (offset) {
0L -> "(UTC\u00B100:00) ${tz.id} ${tz.displayName}"
else -> {
val format = when {
offset > 0 -> "(UTC+%02d:%02d) %s %s"
else -> "(UTC-%02d:%02d) %s %s"
}
offset = abs(offset)
val hours = TimeUnit.MILLISECONDS.toHours(offset)
val minutes =
TimeUnit.MILLISECONDS.toMinutes(offset) - TimeUnit.HOURS.toMinutes(hours)
String.format(format, hours, minutes, tz.id, tz.displayName)
}
}
} catch (ex: Throwable) {
log.w(ex)
return "(incorrect TimeZone)"
}
}
fun onActivityStart() {
this.notifyDataSetChanged()
}
init {
checkVersion(actMain.applicationContext, handler)
ListView(actMain).apply {
adapter = this@SideMenuAdapter
layoutParams = FrameLayout.LayoutParams(
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
backgroundColor = actMain.attrColor(R.attr.colorWindowBackground)
selector = StateListDrawable()
divider = null
dividerHeight = 0
isScrollbarFadingEnabled = false
val padV = (actMain.density * 12f + 0.5f).toInt()
setPadding(0, padV, 0, padV)
clipToPadding = false
scrollBarStyle = ListView.SCROLLBARS_OUTSIDE_OVERLAY
navigationView.addView(this)
}
}
}