more split...

This commit is contained in:
tateisu 2021-06-24 11:31:34 +09:00
parent 6de96b4852
commit 9395774a48
33 changed files with 1158 additions and 1195 deletions

View File

@ -7,10 +7,8 @@ import android.content.SharedPreferences
import android.content.res.Configuration import android.content.res.Configuration
import android.graphics.Typeface import android.graphics.Typeface
import android.os.* import android.os.*
import android.text.InputType
import android.text.Spannable import android.text.Spannable
import android.view.* import android.view.*
import android.view.inputmethod.EditorInfo
import android.widget.* import android.widget.*
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.GravityCompat import androidx.core.view.GravityCompat
@ -40,8 +38,7 @@ class ActMain : AppCompatActivity(),
MyClickableSpanHandler { MyClickableSpanHandler {
companion object { companion object {
private val log = LogCategory("ActMain")
val log = LogCategory("ActMain")
// リザルト // リザルト
const val RESULT_APP_DATA_IMPORT = Activity.RESULT_FIRST_USER const val RESULT_APP_DATA_IMPORT = Activity.RESULT_FIRST_USER
@ -122,9 +119,7 @@ class ActMain : AppCompatActivity(),
var quickTootVisibility: TootVisibility = TootVisibility.AccountSetting var quickTootVisibility: TootVisibility = TootVisibility.AccountSetting
////////////////////////////////////////////////////////////////// lateinit var llFormRoot: LinearLayout
// 変更しない変数(lateinit)
lateinit var llQuickTootBar: LinearLayout lateinit var llQuickTootBar: LinearLayout
lateinit var etQuickToot: MyEditText lateinit var etQuickToot: MyEditText
lateinit var btnQuickToot: ImageButton lateinit var btnQuickToot: ImageButton
@ -879,106 +874,11 @@ class ActMain : AppCompatActivity(),
return rv return rv
} }
internal fun initUI() { // lateinitなビュー変数を初期化する
setContentView(R.layout.act_main) fun findViews() {
App1.initEdgeToEdge(this) llFormRoot = findViewById(R.id.llFormRoot)
quickTootVisibility =
TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility(pref))
?: quickTootVisibility
Column.reloadDefaultColor(this, pref)
var sv = PrefS.spTimelineFont(pref)
if (sv.isNotEmpty()) {
try {
timelineFont = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
}
sv = PrefS.spTimelineFontBold(pref)
if (sv.isNotEmpty()) {
try {
timeline_font_bold = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
} else {
try {
timeline_font_bold = Typeface.create(timelineFont, Typeface.BOLD)
} catch (ex: Throwable) {
log.trace(ex)
}
}
fun parseIconSize(stringPref: StringPref, minDp: Float = 1f): Int {
var iconSizeDp = stringPref.defVal.toFloat()
try {
sv = stringPref(pref)
val fv = if (sv.isEmpty()) Float.NaN else sv.toFloat()
if (fv.isFinite() && fv >= minDp) {
iconSizeDp = fv
}
} catch (ex: Throwable) {
log.trace(ex)
}
return (0.5f + iconSizeDp * density).toInt()
}
avatarIconSize = parseIconSize(PrefS.spAvatarIconSize)
notificationTlIconSize = parseIconSize(PrefS.spNotificationTlIconSize)
boostButtonSize = parseIconSize(PrefS.spBoostButtonSize)
replyIconSize = parseIconSize(PrefS.spReplyIconSize)
headerIconSize = parseIconSize(PrefS.spHeaderIconSize)
stripIconSize = parseIconSize(PrefS.spStripIconSize)
screenBottomPadding = parseIconSize(PrefS.spScreenBottomPadding, minDp = 0f)
run {
var roundRatio = 33f
try {
if (PrefB.bpDontRound(pref)) {
roundRatio = 0f
} else {
sv = PrefS.spRoundRatio(pref)
if (sv.isNotEmpty()) {
val fv = sv.toFloat()
if (fv.isFinite()) {
roundRatio = fv
}
}
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.round_ratio = clipRange(0f, 1f, roundRatio / 100f) * 0.5f
}
run {
var boostAlpha = 0.8f
try {
val f = (PrefS.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f
boostAlpha = when {
f >= 1f -> 1f
f < 0f -> 0.66f
else -> f
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.boostAlpha = boostAlpha
}
llEmpty = findViewById(R.id.llEmpty) llEmpty = findViewById(R.id.llEmpty)
drawer = findViewById(R.id.drawer_layout) drawer = findViewById(R.id.drawer_layout)
drawer.addDrawerListener(this)
drawer.setExclusionSize(stripIconSize)
SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)
btnMenu = findViewById(R.id.btnMenu) btnMenu = findViewById(R.id.btnMenu)
btnToot = findViewById(R.id.btnToot) btnToot = findViewById(R.id.btnToot)
vFooterDivider1 = findViewById(R.id.vFooterDivider1) vFooterDivider1 = findViewById(R.id.vFooterDivider1)
@ -990,128 +890,58 @@ class ActMain : AppCompatActivity(),
btnQuickToot = findViewById(R.id.btnQuickToot) btnQuickToot = findViewById(R.id.btnQuickToot)
btnQuickTootMenu = findViewById(R.id.btnQuickTootMenu) btnQuickTootMenu = findViewById(R.id.btnQuickTootMenu)
val llFormRoot: LinearLayout = findViewById(R.id.llFormRoot)
llFormRoot.setPadding(0, 0, 0, screenBottomPadding)
etQuickToot.typeface = timelineFont
when (PrefI.ipJustifyWindowContentPortrait(pref)) {
PrefI.JWCP_START -> {
val iconW = (stripIconSize * 1.5f + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW
fun ViewGroup.addViewBeforeLast(v: View) = addView(v, childCount - 1)
(svColumnStrip.parent as LinearLayout).addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
PrefI.JWCP_END -> {
val iconW = (stripIconSize * 1.5f + 0.5f).toInt()
val borderWidth = (1f * density + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth
fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1)
(svColumnStrip.parent as LinearLayout).addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
}
if (!PrefB.bpQuickTootBar(pref)) {
llQuickTootBar.visibility = View.GONE
}
btnToot.setOnClickListener(this) btnToot.setOnClickListener(this)
btnMenu.setOnClickListener(this) btnMenu.setOnClickListener(this)
btnQuickToot.setOnClickListener(this) btnQuickToot.setOnClickListener(this)
btnQuickTootMenu.setOnClickListener(this) btnQuickTootMenu.setOnClickListener(this)
}
if (PrefB.bpDontUseActionButtonWithQuickTootBar(pref)) { internal fun initUI() {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE setContentView(R.layout.act_main)
etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE App1.initEdgeToEdge(this)
// 最後に指定する必要がある?
etQuickToot.maxLines = 5 quickTootVisibility =
etQuickToot.isVerticalScrollBarEnabled = true TootVisibility.parseSavedVisibility(PrefS.spQuickTootVisibility(pref))
etQuickToot.isScrollbarFadingEnabled = false ?: quickTootVisibility
} else {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT Column.reloadDefaultColor(this, pref)
etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND
etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ -> reloadFonts()
if (actionId == EditorInfo.IME_ACTION_SEND) { reloadIconSize()
btnQuickToot.performClick() reloadRoundRatio()
return@OnEditorActionListener true reloadBoostAlpha()
}
false findViews()
})
// 最後に指定する必要がある? drawer.addDrawerListener(this)
etQuickToot.maxLines = 1 drawer.setExclusionSize(stripIconSize)
}
SideMenuAdapter(this, handler, findViewById(R.id.nav_view), drawer)
llFormRoot.setPadding(0, 0, 0, screenBottomPadding)
justifyWindowContentPortrait()
initUIQuickToot()
svColumnStrip.isHorizontalFadingEdgeEnabled = true svColumnStrip.isHorizontalFadingEdgeEnabled = true
completionHelper = CompletionHelper(this, pref, appState.handler) completionHelper = CompletionHelper(this, pref, appState.handler)
val dm = resources.displayMetrics val dm = resources.displayMetrics
val density = dm.density val density = dm.density
reloadMediaHeight()
var mediaThumbHeightDp = 64 val columnWMin = loadColumnMin(density)
sv = PrefS.spMediaThumbHeight(pref)
if (sv.isNotEmpty()) {
try {
val iv = Integer.parseInt(sv)
if (iv >= 32) {
mediaThumbHeightDp = iv
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
appState.mediaThumbHeight = (0.5f + mediaThumbHeightDp * density).toInt()
var columnWMinDp = COLUMN_WIDTH_MIN_DP
sv = PrefS.spColumnWidth(pref)
if (sv.isNotEmpty()) {
try {
val iv = Integer.parseInt(sv)
if (iv >= 100) {
columnWMinDp = iv
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
val columnWMin = (0.5f + columnWMinDp * density).toInt()
val sw = dm.widthPixels val sw = dm.widthPixels
// スマホモードとタブレットモードの切り替え
if (PrefB.bpDisableTabletMode(pref) || sw < columnWMin * 2) { if (PrefB.bpDisableTabletMode(pref) || sw < columnWMin * 2) {
// SmartPhone mode
phoneViews = PhoneViews(this) phoneViews = PhoneViews(this)
} else { } else {
// Tablet mode
tabletViews = TabletViews(this) tabletViews = TabletViews(this)
} }
val tmpPhonePager: MyViewPager = findViewById(R.id.viewPager) val tmpPhonePager: MyViewPager = findViewById(R.id.viewPager)
val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager) val tmpTabletPager: RecyclerView = findViewById(R.id.rvPager)
phoneTab({ env -> phoneTab({ env ->
tmpTabletPager.visibility = View.GONE tmpTabletPager.visibility = View.GONE
env.initUI(tmpPhonePager) env.initUI(tmpPhonePager)
@ -1119,23 +949,8 @@ class ActMain : AppCompatActivity(),
}, { env -> }, { env ->
tmpPhonePager.visibility = View.GONE tmpPhonePager.visibility = View.GONE
env.initUI(tmpTabletPager) env.initUI(tmpTabletPager)
}) })
showFooterColor() showFooterColor()
completionHelper.attachEditText(
llFormRoot,
etQuickToot,
true,
object : CompletionHelper.Callback2 {
override fun onTextUpdate() {}
override fun canOpenPopup(): Boolean {
return !drawer.isDrawerOpen(GravityCompat.START)
}
})
showQuickTootVisibility()
} }
} }

View File

@ -63,4 +63,3 @@ fun ActMain.refreshAfterPost() {
this.postedAcct = null this.postedAcct = null
this.postedStatusId = null this.postedStatusId = null
} }

View File

@ -1,20 +1,14 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.content.Intent
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.View import android.view.View
import android.widget.LinearLayout import android.widget.LinearLayout
import android.widget.TextView import android.widget.TextView
import androidx.annotation.RawRes import androidx.annotation.RawRes
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.recyclerview.widget.RecyclerView
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import kotlin.math.abs
import kotlin.math.min
fun ActMain.resizeAutoCW(columnW: Int) { fun ActMain.resizeAutoCW(columnW: Int) {
val sv = PrefS.spAutoCWLines(pref) val sv = PrefS.spAutoCWLines(pref)
@ -144,4 +138,4 @@ fun ActMain.closeListItemPopup() {
} catch (ignored: Throwable) { } catch (ignored: Throwable) {
} }
listItemPopup = null listItemPopup = null
} }

View File

@ -1,9 +1,15 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.text.InputType
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import androidx.core.view.GravityCompat
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.api.entity.TootVisibility import jp.juggler.subwaytooter.api.entity.TootVisibility
import jp.juggler.subwaytooter.dialog.pickAccount import jp.juggler.subwaytooter.dialog.pickAccount
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.CompletionHelper
import jp.juggler.subwaytooter.util.PostCompleteCallback import jp.juggler.subwaytooter.util.PostCompleteCallback
import jp.juggler.subwaytooter.util.PostImpl import jp.juggler.subwaytooter.util.PostImpl
import jp.juggler.util.hideKeyboard import jp.juggler.util.hideKeyboard
@ -14,6 +20,49 @@ import org.jetbrains.anko.imageResource
val ActMain.quickTootText: String val ActMain.quickTootText: String
get() = etQuickToot.text.toString() get() = etQuickToot.text.toString()
fun ActMain.initUIQuickToot() {
etQuickToot.typeface = ActMain.timelineFont
if (!PrefB.bpQuickTootBar(pref)) {
llQuickTootBar.visibility = View.GONE
}
if (PrefB.bpDontUseActionButtonWithQuickTootBar(pref)) {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_FLAG_MULTI_LINE
etQuickToot.imeOptions = EditorInfo.IME_ACTION_NONE
// 最後に指定する必要がある?
etQuickToot.maxLines = 5
etQuickToot.isVerticalScrollBarEnabled = true
etQuickToot.isScrollbarFadingEnabled = false
} else {
etQuickToot.inputType = InputType.TYPE_CLASS_TEXT
etQuickToot.imeOptions = EditorInfo.IME_ACTION_SEND
etQuickToot.setOnEditorActionListener(TextView.OnEditorActionListener { _, actionId, _ ->
if (actionId == EditorInfo.IME_ACTION_SEND) {
btnQuickToot.performClick()
return@OnEditorActionListener true
}
false
})
// 最後に指定する必要がある?
etQuickToot.maxLines = 1
}
completionHelper.attachEditText(
llFormRoot,
etQuickToot,
true,
object : CompletionHelper.Callback2 {
override fun onTextUpdate() {}
override fun canOpenPopup(): Boolean {
return !drawer.isDrawerOpen(GravityCompat.START)
}
})
showQuickTootVisibility()
}
fun ActMain.showQuickTootVisibility() { fun ActMain.showQuickTootVisibility() {
btnQuickTootMenu.imageResource = btnQuickTootMenu.imageResource =
when (val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) { when (val resId = Styler.getVisibilityIconId(false, quickTootVisibility)) {

View File

@ -1,19 +1,183 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.content.res.ColorStateList import android.content.res.ColorStateList
import android.graphics.Typeface
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import jp.juggler.subwaytooter.api.entity.TootStatus import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
import jp.juggler.subwaytooter.util.CustomShare import jp.juggler.subwaytooter.util.CustomShare
import jp.juggler.subwaytooter.view.ListDivider import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.subwaytooter.view.TabletColumnDivider import jp.juggler.subwaytooter.view.TabletColumnDivider
import jp.juggler.util.attrColor import jp.juggler.util.*
import jp.juggler.util.getAdaptiveRippleDrawableRound
import jp.juggler.util.notZero
import org.jetbrains.anko.backgroundDrawable import org.jetbrains.anko.backgroundDrawable
import java.util.* import java.util.*
private val log = LogCategory("ActMainStyle")
// initUIから呼ばれる
fun ActMain.reloadFonts() {
var sv = PrefS.spTimelineFont(pref)
if (sv.isNotEmpty()) {
try {
ActMain.timelineFont = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
}
sv = PrefS.spTimelineFontBold(pref)
if (sv.isNotEmpty()) {
try {
ActMain.timeline_font_bold = Typeface.createFromFile(sv)
} catch (ex: Throwable) {
log.trace(ex)
}
} else {
try {
ActMain.timeline_font_bold = Typeface.create(ActMain.timelineFont, Typeface.BOLD)
} catch (ex: Throwable) {
log.trace(ex)
}
}
}
fun ActMain.parseIconSize(stringPref: StringPref, minDp: Float = 1f): Int {
var iconSizeDp = stringPref.defVal.toFloat()
try {
val sv = stringPref(pref)
val fv = if (sv.isEmpty()) Float.NaN else sv.toFloat()
if (fv.isFinite() && fv >= minDp) {
iconSizeDp = fv
}
} catch (ex: Throwable) {
log.trace(ex)
}
return (0.5f + iconSizeDp * density).toInt()
}
// initUIから呼ばれる
fun ActMain.reloadIconSize() {
avatarIconSize = parseIconSize(PrefS.spAvatarIconSize)
notificationTlIconSize = parseIconSize(PrefS.spNotificationTlIconSize)
ActMain.boostButtonSize = parseIconSize(PrefS.spBoostButtonSize)
ActMain.replyIconSize = parseIconSize(PrefS.spReplyIconSize)
ActMain.headerIconSize = parseIconSize(PrefS.spHeaderIconSize)
ActMain.stripIconSize = parseIconSize(PrefS.spStripIconSize)
ActMain.screenBottomPadding = parseIconSize(PrefS.spScreenBottomPadding, minDp = 0f)
}
// initUIから呼ばれる
fun ActMain.reloadRoundRatio() {
var roundRatio = 33f
try {
if (PrefB.bpDontRound(pref)) {
roundRatio = 0f
} else {
val sv = PrefS.spRoundRatio(pref)
if (sv.isNotEmpty()) {
val fv = sv.toFloat()
if (fv.isFinite()) {
roundRatio = fv
}
}
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.round_ratio = clipRange(0f, 1f, roundRatio / 100f) * 0.5f
}
// initUI から呼ばれる
fun ActMain.reloadBoostAlpha() {
var boostAlpha = 0.8f
try {
val f = (PrefS.spBoostAlpha.toInt(pref).toFloat() + 0.5f) / 100f
boostAlpha = when {
f >= 1f -> 1f
f < 0f -> 0.66f
else -> f
}
} catch (ex: Throwable) {
log.trace(ex)
}
Styler.boostAlpha = boostAlpha
}
fun ActMain.reloadMediaHeight() {
var mediaThumbHeightDp = 64
val sv = PrefS.spMediaThumbHeight(pref)
if (sv.isNotEmpty()) {
try {
val iv = Integer.parseInt(sv)
if (iv >= 32) mediaThumbHeightDp = iv
} catch (ex: Throwable) {
log.trace(ex)
}
}
appState.mediaThumbHeight = (0.5f + mediaThumbHeightDp * density).toInt()
}
fun ActMain.loadColumnMin(density: Float): Int {
var x = ActMain.COLUMN_WIDTH_MIN_DP.toFloat()
val sv = PrefS.spColumnWidth(pref)
if (sv.isNotEmpty()) {
try {
val fv = sv.toFloat()
if (fv.isFinite() && fv >= 100f) {
x = fv
}
} catch (ex: Throwable) {
log.trace(ex)
}
}
return (0.5f + x * density).toInt()
}
fun ActMain.justifyWindowContentPortrait() {
when (PrefI.ipJustifyWindowContentPortrait(pref)) {
PrefI.JWCP_START -> {
val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW
fun ViewGroup.addViewBeforeLast(v: View) = addView(v, childCount - 1)
(svColumnStrip.parent as LinearLayout).addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewBeforeLast(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
PrefI.JWCP_END -> {
val iconW = (ActMain.stripIconSize * 1.5f + 0.5f).toInt()
val borderWidth = (1f * density + 0.5f).toInt()
val padding = resources.displayMetrics.widthPixels / 2 - iconW - borderWidth
fun ViewGroup.addViewAfterFirst(v: View) = addView(v, 1)
(svColumnStrip.parent as LinearLayout).addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
llQuickTootBar.addViewAfterFirst(
View(this).apply {
layoutParams = LinearLayout.LayoutParams(padding, 0)
}
)
}
}
}
//////////////////////////////////////////////////////
// onStart時に呼ばれる // onStart時に呼ばれる
fun ActMain.reloadTimeZone(){ fun ActMain.reloadTimeZone() {
try { try {
var tz = TimeZone.getDefault() var tz = TimeZone.getDefault()
val tzId = PrefS.spTimeZone(pref) val tzId = PrefS.spTimeZone(pref)
@ -22,13 +186,13 @@ fun ActMain.reloadTimeZone(){
} }
TootStatus.date_format.timeZone = tz TootStatus.date_format.timeZone = tz
} catch (ex: Throwable) { } catch (ex: Throwable) {
ActMain.log.e(ex, "getTimeZone failed.") log.e(ex, "getTimeZone failed.")
} }
} }
// onStart時に呼ばれる // onStart時に呼ばれる
// カラーカスタマイズを読み直す // カラーカスタマイズを読み直す
fun ActMain.reloadColors(){ fun ActMain.reloadColors() {
ListDivider.color = PrefI.ipListDividerColor(pref) ListDivider.color = PrefI.ipListDividerColor(pref)
TabletColumnDivider.color = PrefI.ipListDividerColor(pref) TabletColumnDivider.color = PrefI.ipListDividerColor(pref)
ItemViewHolder.toot_color_unlisted = PrefI.ipTootColorUnlisted(pref) ItemViewHolder.toot_color_unlisted = PrefI.ipTootColorUnlisted(pref)

View File

@ -30,6 +30,7 @@ import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.util.* import jp.juggler.util.*
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import okhttp3.Request import okhttp3.Request
import okhttp3.internal.closeQuietly
import ru.gildor.coroutines.okhttp.await import ru.gildor.coroutines.okhttp.await
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
@ -40,7 +41,6 @@ class ActPost : AppCompatActivity(),
MyClickableSpanHandler, AttachmentPicker.Callback { MyClickableSpanHandler, AttachmentPicker.Callback {
companion object { companion object {
internal val log = LogCategory("ActPost") internal val log = LogCategory("ActPost")
var refActPost: WeakReference<ActPost>? = null var refActPost: WeakReference<ActPost>? = null
@ -69,8 +69,8 @@ class ActPost : AppCompatActivity(),
///////////////////////////////////////////////// /////////////////////////////////////////////////
fun createIntent( fun createIntent(
activity: Activity, activity: Activity,
accountDbId: Long, accountDbId: Long,
multiWindowMode: Boolean, multiWindowMode: Boolean,
@ -127,9 +127,12 @@ class ActPost : AppCompatActivity(),
val request = Request.Builder().url(url).build() val request = Request.Builder().url(url).build()
val call = App1.ok_http_client.newCall(request) val call = App1.ok_http_client.newCall(request)
val response = call.await() val response = call.await()
if (response.isSuccessful) return true try {
if (response.isSuccessful) return true
log.e(TootApiClient.formatResponse(response, "check_exist failed.")) log.e(TootApiClient.formatResponse(response, "check_exist failed."))
} finally {
response.closeQuietly()
}
} catch (ex: Throwable) { } catch (ex: Throwable) {
log.trace(ex) log.trace(ex)
} }
@ -165,7 +168,7 @@ class ActPost : AppCompatActivity(),
lateinit var cbQuote: CheckBox lateinit var cbQuote: CheckBox
lateinit var spEnquete: Spinner lateinit var spPollType: Spinner
lateinit var llEnquete: View lateinit var llEnquete: View
lateinit var etChoices: List<MyEditText> lateinit var etChoices: List<MyEditText>
@ -197,8 +200,6 @@ class ActPost : AppCompatActivity(),
/////////////////////////////////////////////////// ///////////////////////////////////////////////////
var states = ActPostStates() var states = ActPostStates()
internal var account: SavedAccount? = null internal var account: SavedAccount? = null
@ -220,18 +221,6 @@ class ActPost : AppCompatActivity(),
var paThumbnailTarget: PostAttachment? = null var paThumbnailTarget: PostAttachment? = null
val textWatcher: TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
updateTextCount()
}
}
val scrollListener: ViewTreeObserver.OnScrollChangedListener = val scrollListener: ViewTreeObserver.OnScrollChangedListener =
ViewTreeObserver.OnScrollChangedListener { completionHelper.onScrollChanged() } ViewTreeObserver.OnScrollChangedListener { completionHelper.onScrollChanged() }
@ -292,24 +281,65 @@ class ActPost : AppCompatActivity(),
//////////////////////////////////////////////////////////////// ////////////////////////////////////////////////////////////////
override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { override fun onCreate(savedInstanceState: Bundle?) {
return when { super.onCreate(savedInstanceState)
super.onKeyDown(keyCode, event) -> true if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this))
event == null -> false App1.setActivityTheme(this, noActionBar = true)
else -> event.isCtrlPressed appState = App1.getAppState(this)
handler = appState.handler
pref = appState.pref
attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this)
density = resources.displayMetrics.density
arMushroom.register(this, log)
initUI()
when (savedInstanceState) {
null -> updateText(intent, confirmed = true, saveDraft = false)
else -> restoreState(savedInstanceState)
} }
} }
override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean { override fun onDestroy() {
val rv = super.onKeyUp(keyCode, event) completionHelper.onDestroy()
if (event?.isCtrlPressed == true) { attachmentUploader.onActivityDestroy()
ActMain.log.d("onKeyUp code=$keyCode rv=$rv") super.onDestroy()
when (keyCode) { }
KeyEvent.KEYCODE_T -> btnPost.performClick()
} override fun onSaveInstanceState(outState: Bundle) {
return true super.onSaveInstanceState(outState)
} saveState(outState)
return rv }
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
showContentWarningEnabled()
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
showPoll()
showQuotedRenote()
}
override fun onResume() {
super.onResume()
refActPost = WeakReference(this)
}
override fun onPause() {
super.onPause()
// 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する
// やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると
// メイン画面より上にあるアクティビティはすべて消されてしまうので
// このタイミングで保存するしかない
if (!isPostComplete) saveDraft()
}
override fun onBackPressed() {
saveDraft()
super.onBackPressed()
} }
override fun onClick(v: View) { override fun onClick(v: View) {
@ -335,93 +365,31 @@ class ActPost : AppCompatActivity(),
} }
} }
// unused? for REQUEST_CODE_ATTACHMENT override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean {
// fun handleAttachmentResult(ar: ActivityResult?) { return when {
// if (ar?.resultCode == RESULT_OK) { super.onKeyDown(keyCode, event) -> true
// ar.data?.handleGetContentResult(contentResolver)?.let { checkAttachments(it) } event == null -> false
// } else -> event.isCtrlPressed
// }
override fun onBackPressed() {
saveDraft()
super.onBackPressed()
}
override fun onResume() {
super.onResume()
refActPost = WeakReference(this)
}
override fun onPause() {
super.onPause()
// 編集中にホーム画面を押したり他アプリに移動する場合は下書きを保存する
// やや過剰な気がするが、自アプリに戻ってくるときにランチャーからアイコンタップされると
// メイン画面より上にあるアクティビティはすべて消されてしまうので
// このタイミングで保存するしかない
if (!isPostComplete) {
saveDraft()
} }
} }
override fun onCreate(savedInstanceState: Bundle?) { override fun onKeyUp(keyCode: Int, event: KeyEvent?): Boolean {
val rv = super.onKeyUp(keyCode, event)
super.onCreate(savedInstanceState) if (event?.isCtrlPressed == true) {
ActMain.log.d("onKeyUp code=$keyCode rv=$rv")
if (isMultiWindowPost) ActMain.refActMain?.get()?.closeList?.add(WeakReference(this)) when (keyCode) {
KeyEvent.KEYCODE_T -> btnPost.performClick()
App1.setActivityTheme(this, noActionBar = true) }
return true
appState = App1.getAppState(this)
handler = appState.handler
pref = appState.pref
attachmentUploader = AttachmentUploader(this, handler)
attachmentPicker = AttachmentPicker(this, this)
arMushroom.register(this, log)
initUI()
if (savedInstanceState != null) {
restoreState(savedInstanceState)
} else {
updateText(intent, confirmed = true, saveDraft = false)
} }
} return rv
override fun onDestroy() {
completionHelper.onDestroy()
attachmentUploader.onActivityDestroy()
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
saveState(outState)
}
override fun onRestoreInstanceState(savedInstanceState: Bundle) {
super.onRestoreInstanceState(savedInstanceState)
showContentWarningEnabled()
showMediaAttachment()
showVisibility()
updateTextCount()
showReplyTo()
showPoll()
showQuotedRenote()
} }
override fun onMyClickableSpanClicked(viewClicked: View, span: MyClickableSpan) { override fun onMyClickableSpanClicked(viewClicked: View, span: MyClickableSpan) {
openBrowser(span.linkInfo.url) openBrowser(span.linkInfo.url)
} }
override fun onRequestPermissionsResult( override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
requestCode: Int,
permissions: Array<String>,
grantResults: IntArray,
) {
attachmentPicker.onRequestPermissionsResult(requestCode, permissions, grantResults) attachmentPicker.onRequestPermissionsResult(requestCode, permissions, grantResults)
super.onRequestPermissionsResult(requestCode, permissions, grantResults) super.onRequestPermissionsResult(requestCode, permissions, grantResults)
} }
@ -439,12 +407,10 @@ class ActPost : AppCompatActivity(),
} }
fun initUI() { fun initUI() {
density = resources.displayMetrics.density
setContentView(R.layout.act_post) setContentView(R.layout.act_post)
App1.initEdgeToEdge(this) App1.initEdgeToEdge(this)
if (PrefB.bpPostButtonBarTop(this)) { if (PrefB.bpPostButtonBarTop(pref)) {
val bar = findViewById<View>(R.id.llFooterBar) val bar = findViewById<View>(R.id.llFooterBar)
val parent = bar.parent as ViewGroup val parent = bar.parent as ViewGroup
parent.removeView(bar) parent.removeView(bar)
@ -480,7 +446,7 @@ class ActPost : AppCompatActivity(),
cbQuote = findViewById(R.id.cbQuote) cbQuote = findViewById(R.id.cbQuote)
spEnquete = findViewById<Spinner>(R.id.spEnquete).apply { spPollType = findViewById<Spinner>(R.id.spEnquete).apply {
this.adapter = ArrayAdapter( this.adapter = ArrayAdapter(
this@ActPost, this@ActPost,
android.R.layout.simple_spinner_item, android.R.layout.simple_spinner_item,
@ -499,17 +465,13 @@ class ActPost : AppCompatActivity(),
updateTextCount() updateTextCount()
} }
override fun onItemSelected( override fun onItemSelected(parent: AdapterView<*>?, view: View?, position: Int, id: Long) {
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long,
) {
showPoll() showPoll()
updateTextCount() updateTextCount()
} }
} }
} }
llEnquete = findViewById(R.id.llEnquete) llEnquete = findViewById(R.id.llEnquete)
llExpire = findViewById(R.id.llExpire) llExpire = findViewById(R.id.llExpire)
cbHideTotals = findViewById(R.id.cbHideTotals) cbHideTotals = findViewById(R.id.cbHideTotals)
@ -542,7 +504,6 @@ class ActPost : AppCompatActivity(),
ibSchedule = findViewById(R.id.ibSchedule) ibSchedule = findViewById(R.id.ibSchedule)
ibScheduleReset = findViewById(R.id.ibScheduleReset) ibScheduleReset = findViewById(R.id.ibScheduleReset)
arrayOf( arrayOf(
ibSchedule, ibSchedule,
ibScheduleReset, ibScheduleReset,
@ -559,9 +520,7 @@ class ActPost : AppCompatActivity(),
ivMedia.forEach { it.setOnClickListener(this) } ivMedia.forEach { it.setOnClickListener(this) }
cbContentWarning.setOnCheckedChangeListener { _, _ -> cbContentWarning.setOnCheckedChangeListener { _, _ -> showContentWarningEnabled() }
showContentWarningEnabled()
}
completionHelper = CompletionHelper(this, pref, appState.handler) completionHelper = CompletionHelper(this, pref, appState.handler)
completionHelper.attachEditText(formRoot, etContent, false, object : CompletionHelper.Callback2 { completionHelper.attachEditText(formRoot, etContent, false, object : CompletionHelper.Callback2 {
@ -569,12 +528,23 @@ class ActPost : AppCompatActivity(),
updateTextCount() updateTextCount()
} }
override fun canOpenPopup(): Boolean { override fun canOpenPopup(): Boolean = true
return true
}
}) })
val textWatcher: TextWatcher = object : TextWatcher {
override fun beforeTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun onTextChanged(charSequence: CharSequence, i: Int, i1: Int, i2: Int) {
}
override fun afterTextChanged(editable: Editable) {
updateTextCount()
}
}
etContentWarning.addTextChangedListener(textWatcher) etContentWarning.addTextChangedListener(textWatcher)
for (et in etChoices) { for (et in etChoices) {
et.addTextChangedListener(textWatcher) et.addTextChangedListener(textWatcher)
} }

View File

@ -38,7 +38,6 @@ fun ActPost.decodeAttachments(sv: String) {
} }
} }
fun ActPost.showMediaAttachment() { fun ActPost.showMediaAttachment() {
if (isFinishing) return if (isFinishing) return
llAttachment.vg(attachmentList.isNotEmpty()) llAttachment.vg(attachmentList.isNotEmpty())

View File

@ -74,7 +74,7 @@ fun ActPost.updateTextCount() {
} }
} }
when (spEnquete.selectedItemPosition) { when (spPollType.selectedItemPosition) {
1 -> checkEnqueteLength() 1 -> checkEnqueteLength()
2 -> { 2 -> {

View File

@ -102,7 +102,7 @@ fun ActPost.resetText() {
attachmentList.clear() attachmentList.clear()
cbQuote.isChecked = false cbQuote.isChecked = false
etContent.setText("") etContent.setText("")
spEnquete.setSelection(0, false) spPollType.setSelection(0, false)
etChoices.forEach { it.setText("") } etChoices.forEach { it.setText("") }
accountList = SavedAccount.loadAccountList(this) accountList = SavedAccount.loadAccountList(this)
SavedAccount.sort(accountList) SavedAccount.sort(accountList)
@ -132,7 +132,6 @@ fun ActPost.afterUpdateText() {
updateTextCount() updateTextCount()
} }
// 初期化時と投稿完了時とリセット確認後に呼ばれる // 初期化時と投稿完了時とリセット確認後に呼ばれる
fun ActPost.updateText( fun ActPost.updateText(
intent: Intent, intent: Intent,
@ -294,7 +293,7 @@ fun ActPost.performPost() {
var pollExpireSeconds = 0 var pollExpireSeconds = 0
var pollHideTotals = false var pollHideTotals = false
var pollMultipleChoice = false var pollMultipleChoice = false
when (spEnquete.selectedItemPosition) { when (spPollType.selectedItemPosition) {
1 -> { 1 -> {
pollType = TootPollsType.Mastodon pollType = TootPollsType.Mastodon
pollItems = pollChoiceList() pollItems = pollChoiceList()

View File

@ -5,7 +5,7 @@ import jp.juggler.util.notEmpty
import jp.juggler.util.vg import jp.juggler.util.vg
fun ActPost.showPoll() { fun ActPost.showPoll() {
val i = spEnquete.selectedItemPosition val i = spPollType.selectedItemPosition
llEnquete.vg(i != 0) llEnquete.vg(i != 0)
llExpire.vg(i == 1) llExpire.vg(i == 1)
cbHideTotals.vg(i == 1) cbHideTotals.vg(i == 1)
@ -13,9 +13,9 @@ fun ActPost.showPoll() {
} }
// 投票が有効で何か入力済みなら真 // 投票が有効で何か入力済みなら真
fun ActPost.hasPoll():Boolean{ fun ActPost.hasPoll(): Boolean {
if( spEnquete.selectedItemPosition <= 0) return false if (spPollType.selectedItemPosition <= 0) return false
return etChoices.any{ it.text.toString().isNotBlank()} return etChoices.any { it.text.toString().isNotBlank() }
} }
fun ActPost.pollChoiceList() = ArrayList<String>().apply { fun ActPost.pollChoiceList() = ArrayList<String>().apply {
@ -30,4 +30,3 @@ fun ActPost.pollExpireSeconds(): Int {
val m = etExpireMinutes.text.toString().trim().toDoubleOrNull().finiteOrZero() val m = etExpireMinutes.text.toString().trim().toDoubleOrNull().finiteOrZero()
return (d * 86400.0 + h * 3600.0 + m * 60.0).toInt() return (d * 86400.0 + h * 3600.0 + m * 60.0).toInt()
} }

View File

@ -38,13 +38,12 @@ private const val DRAFT_POLL_EXPIRE_MINUTE = "poll_expire_minute"
private const val DRAFT_ENQUETE_ITEMS = "enquete_items" private const val DRAFT_ENQUETE_ITEMS = "enquete_items"
private const val DRAFT_QUOTE = "quotedRenote" // 歴史的な理由で名前がMisskey用になってる private const val DRAFT_QUOTE = "quotedRenote" // 歴史的な理由で名前がMisskey用になってる
fun ActPost.saveDraft() { fun ActPost.saveDraft() {
val content = etContent.text.toString() val content = etContent.text.toString()
val contentWarning = val contentWarning =
if (cbContentWarning.isChecked) etContentWarning.text.toString() else "" if (cbContentWarning.isChecked) etContentWarning.text.toString() else ""
val isEnquete = spEnquete.selectedItemPosition > 0 val isEnquete = spPollType.selectedItemPosition > 0
val strChoice = arrayOf( val strChoice = arrayOf(
if (isEnquete) etChoices[0].text.toString() else "", if (isEnquete) etChoices[0].text.toString() else "",
@ -88,7 +87,7 @@ fun ActPost.saveDraft() {
// deprecated. but still used in old draft. // deprecated. but still used in old draft.
// json.put(DRAFT_IS_ENQUETE, isEnquete) // json.put(DRAFT_IS_ENQUETE, isEnquete)
json[DRAFT_POLL_TYPE] = spEnquete.selectedItemPosition.toPollTypeString() json[DRAFT_POLL_TYPE] = spPollType.selectedItemPosition.toPollTypeString()
json[DRAFT_POLL_MULTIPLE] = cbMultipleChoice.isChecked json[DRAFT_POLL_MULTIPLE] = cbMultipleChoice.isChecked
json[DRAFT_POLL_HIDE_TOTALS] = cbHideTotals.isChecked json[DRAFT_POLL_HIDE_TOTALS] = cbHideTotals.isChecked
@ -236,11 +235,11 @@ fun ActPost.restoreDraft(draft: JsonObject) {
val sv = draft.string(DRAFT_POLL_TYPE) val sv = draft.string(DRAFT_POLL_TYPE)
if (sv != null) { if (sv != null) {
spEnquete.setSelection(sv.toPollTypeIndex()) spPollType.setSelection(sv.toPollTypeIndex())
} else { } else {
// old draft // old draft
val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false) val bv = draft.optBoolean(DRAFT_IS_ENQUETE, false)
spEnquete.setSelection(if (bv) 2 else 0) spPollType.setSelection(if (bv) 2 else 0)
} }
cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE) cbMultipleChoice.isChecked = draft.optBoolean(DRAFT_POLL_MULTIPLE)
@ -369,7 +368,7 @@ fun ActPost.initializeFromRedraftStatus(account: SavedAccount, jsonText: String)
} }
else -> { else -> {
spEnquete.setSelection( spPollType.setSelection(
if (srcEnquete.pollType == TootPollsType.FriendsNico) { if (srcEnquete.pollType == TootPollsType.FriendsNico) {
2 2
} else { } else {

View File

@ -1,6 +1,5 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.annotation.SuppressLint
import android.view.View import android.view.View
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser

View File

@ -1,9 +1,7 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.view.View import android.view.View
import jp.juggler.subwaytooter.api.entity.TootVisibility
fun ActPost.showContentWarningEnabled() { fun ActPost.showContentWarningEnabled() {
etContentWarning.visibility = if (cbContentWarning.isChecked) View.VISIBLE else View.GONE etContentWarning.visibility = if (cbContentWarning.isChecked) View.VISIBLE else View.GONE
} }

View File

@ -611,4 +611,4 @@ class App1 : Application() {
} }
} }
val kJson = kotlinx.serialization.json.Json{ ignoreUnknownKeys = true } val kJson = kotlinx.serialization.json.Json { ignoreUnknownKeys = true }

View File

@ -4,11 +4,10 @@ import android.content.Context
import android.os.Environment import android.os.Environment
import android.util.LruCache import android.util.LruCache
import androidx.annotation.RawRes import androidx.annotation.RawRes
import jp.juggler.subwaytooter.api.ApiPath.READ_LIMIT
import jp.juggler.subwaytooter.Column.Companion.log import jp.juggler.subwaytooter.Column.Companion.log
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.ApiPath.READ_LIMIT
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
import java.io.File import java.io.File
import java.util.* import java.util.*
@ -826,94 +825,6 @@ fun Column.makeProfileStatusesUrl(profileId: EntityId?): String {
return path return path
} }
val misskeyArrayFinderUsers = { it: JsonObject ->
it.jsonArray("users")
}
////////////////////////////////////////////////////////////////////////////////
// account list parser
val nullArrayFinder: (JsonObject) -> JsonArray? =
{ null }
val defaultAccountListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> parser.accountList(jsonArray) }
private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray, key: String) =
srcList.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
?.apply { _orderId = relationId }
}
}
val misskey11FollowingParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") }
val misskey11FollowersParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserFollowRequest: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserMutes: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") }
val misskeyCustomParserBlocks: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") }
////////////////////////////////////////////////////////////////////////////////
// status list parser
val defaultStatusListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootStatus> =
{ parser, jsonArray -> parser.statusList(jsonArray) }
val misskeyCustomParserFavorites: (TootParser, JsonArray) -> List<TootStatus> =
{ parser, jsonArray ->
jsonArray.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> parser.status(it.jsonObject("note"))?.apply {
favourited = true
_orderId = relationId
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// notification list parser
val defaultNotificationListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootNotification> =
{ parser, jsonArray -> parser.notificationList(jsonArray) }
val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootDomainBlock> =
{ _, jsonArray -> TootDomainBlock.parseList(jsonArray) }
val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
{ parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) }
///////////////////////////////////////////////////////////////////////
val mastodonFollowSuggestion2ListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray ->
TootAccountRef.wrapList(parser,
jsonArray.objectList().mapNotNull {
parser.account(it.jsonObject("account"))?.also { a ->
SuggestionSource.set(
(parser.linkHelper as? SavedAccount)?.db_id,
a.acct,
it.string("source")
)
}
}
)
}
/////////////////////////////////////////////////////////////////////// ///////////////////////////////////////////////////////////////////////
private const val DIR_BACKGROUND_IMAGE = "columnBackground" private const val DIR_BACKGROUND_IMAGE = "columnBackground"

View File

@ -7,6 +7,7 @@ import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.streaming.StreamManager import jp.juggler.subwaytooter.streaming.StreamManager
import jp.juggler.subwaytooter.streaming.StreamStatus import jp.juggler.subwaytooter.streaming.StreamStatus
import jp.juggler.subwaytooter.util.ScrollPosition import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.notEmpty
import jp.juggler.util.runOnMainLooper import jp.juggler.util.runOnMainLooper
import kotlin.math.max import kotlin.math.max
@ -73,37 +74,22 @@ fun Column.mergeStreamingMessage() {
lastShowStreamData.set(now) lastShowStreamData.set(now)
// read items while queue is not empty
val tmpList = ArrayList<TimelineItem>() val tmpList = ArrayList<TimelineItem>()
while (true) tmpList.add(streamDataQueue.poll() ?: break) .apply { while (true) add(streamDataQueue.poll() ?: break) }.notEmpty()
if (tmpList.isEmpty()) return ?: return
// キューから読めた件数が0の場合を除き、少し後に再処理させることでマージ漏れを防ぐ // キューから読めた件数が0の場合を除き、少し後に再処理させることでマージ漏れを防ぐ
handler.postDelayed(procMergeStreamingMessage, 333L) handler.postDelayed(procMergeStreamingMessage, 333L)
// ストリーミングされるデータは全てID順に並んでいるはず // orderId順ソートを徹底する
tmpList.sortByDescending { it.getOrderId() } tmpList.sortByDescending { it.getOrderId() }
val listNew = duplicateMap.filterDuplicate(tmpList) // 既にカラム中にあるデータは除去する
if (listNew.isEmpty()) return val listNew = duplicateMap.filterDuplicate(tmpList).notEmpty() ?: return
for (item in listNew) { sendToSpeech(listNew)
if (enableSpeech && item is TootStatus) { injectToPollingWorker(listNew)
appState.addSpeech(item.reblog ?: item)
}
}
// 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
if (isNotificationColumn) {
val list = ArrayList<TootNotification>()
for (o in listNew) {
if (o is TootNotification) {
list.add(o)
}
}
if (list.isNotEmpty()) {
PollingWorker.injectData(context, accessInfo, list)
}
}
// 最新のIDをsince_idとして覚える(ソートはしない) // 最新のIDをsince_idとして覚える(ソートはしない)
var newIdMax: EntityId? = null var newIdMax: EntityId? = null
@ -154,17 +140,7 @@ fun Column.mergeStreamingMessage() {
// 画面復帰時の自動リフレッシュではギャップが残る可能性がある // 画面復帰時の自動リフレッシュではギャップが残る可能性がある
if (bPutGap) { if (bPutGap) {
bPutGap = false bPutGap = false
try { addGapAfterStreaming(listNew, newIdMin)
if (listData.size > 0 && newIdMin != null) {
val since = listData[0].getOrderId()
if (newIdMin > since) {
val gap = TootGap(newIdMin, since)
listNew.add(gap)
}
}
} catch (ex: Throwable) {
Column.log.e(ex, "can't put gap.")
}
} }
val changeList = ArrayList<AdapterChange>() val changeList = ArrayList<AdapterChange>()
@ -192,8 +168,49 @@ fun Column.mergeStreamingMessage() {
listData.addAll(0, listNew) listData.addAll(0, listNew)
fireShowContent(reason = "mergeStreamingMessage", changeList = changeList) fireShowContent(reason = "mergeStreamingMessage", changeList = changeList)
scrollAfterStreaming(added, holderSp, restoreIdx, restoreY)
updateMisskeyCapture()
}
if (holder != null) { // 通知カラムならストリーミング経由で届いたデータを通知ワーカーに伝達する
private fun Column.injectToPollingWorker(listNew: ArrayList<TimelineItem>) {
if (!isNotificationColumn) return
listNew.mapNotNull { it as? TootNotification }.notEmpty()
?.let { PollingWorker.injectData(context, accessInfo, it) }
}
private fun Column.sendToSpeech(listNew: ArrayList<TimelineItem>) {
if (!enableSpeech) return
listNew.mapNotNull { it as? TootStatus }
.forEach { appState.addSpeech(it.reblog ?: it) }
}
private fun Column.addGapAfterStreaming(listNew: ArrayList<TimelineItem>, newIdMin: EntityId?) {
try {
if (listData.size > 0 && newIdMin != null) {
val since = listData[0].getOrderId()
if (newIdMin > since) {
val gap = TootGap(newIdMin, since)
listNew.add(gap)
}
}
} catch (ex: Throwable) {
Column.log.e(ex, "can't put gap.")
}
}
private fun Column.scrollAfterStreaming(added: Int, holderSp: ScrollPosition?, restoreIdx: Int, restoreY: Int) {
val holder = viewHolder
if (holder == null) {
val scrollSave = this.scrollSave
when {
// スクロール位置が先頭なら先頭のまま
scrollSave == null || scrollSave.isHead -> Unit
// 現在の要素が表示され続けるようにしたい
else -> scrollSave.adapterIndex += added
}
} else {
when { when {
holderSp == null -> { holderSp == null -> {
// スクロール位置が先頭なら先頭にする // スクロール位置が先頭なら先頭にする
@ -218,19 +235,7 @@ fun Column.mergeStreamingMessage() {
holder.setListItemTop(restoreIdx + added, restoreY) holder.setListItemTop(restoreIdx + added, restoreY)
} }
} }
} else {
val scrollSave = this.scrollSave
when {
// スクロール位置が先頭なら先頭のまま
scrollSave == null || scrollSave.isHead -> {
}
// 現在の要素が表示され続けるようにしたい
else -> scrollSave.adapterIndex += added
}
} }
updateMisskeyCapture()
} }
fun Column.runOnMainLooperForStreamingEvent(proc: () -> Unit) { fun Column.runOnMainLooperForStreamingEvent(proc: () -> Unit) {

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.os.SystemClock import android.os.SystemClock
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.notification.PollingWorker import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.util.* import jp.juggler.util.*
import java.lang.StringBuilder import java.lang.StringBuilder

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.os.SystemClock import android.os.SystemClock
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.notification.PollingWorker import jp.juggler.subwaytooter.notification.PollingWorker
import jp.juggler.subwaytooter.util.OpenSticker import jp.juggler.subwaytooter.util.OpenSticker
import jp.juggler.util.* import jp.juggler.util.*

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter
import android.os.SystemClock import android.os.SystemClock
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.util.ScrollPosition import jp.juggler.subwaytooter.util.ScrollPosition
import jp.juggler.util.* import jp.juggler.util.*

View File

@ -6,6 +6,7 @@ import jp.juggler.subwaytooter.api.ApiPath
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.finder.*
import jp.juggler.subwaytooter.search.MspHelper.loadingMSP import jp.juggler.subwaytooter.search.MspHelper.loadingMSP
import jp.juggler.subwaytooter.search.MspHelper.refreshMSP import jp.juggler.subwaytooter.search.MspHelper.refreshMSP
import jp.juggler.subwaytooter.search.NotestockHelper.loadingNotestock import jp.juggler.subwaytooter.search.NotestockHelper.loadingNotestock

View File

@ -635,6 +635,7 @@ internal class DlgContextMenu(
return true return true
} }
@Suppress("ComplexMethod")
private fun ActMain.onClickUser(v: View, pos: Int, who: TootAccount, whoRef: TootAccountRef): Boolean { private fun ActMain.onClickUser(v: View, pos: Int, who: TootAccount, whoRef: TootAccountRef): Boolean {
when (v.id) { when (v.id) {
R.id.btnReportUser -> userReportForm(accessInfo, who) R.id.btnReportUser -> userReportForm(accessInfo, who)

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter package jp.juggler.subwaytooter
import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.view.View import android.view.View
import android.widget.ImageView import android.widget.ImageView
@ -51,34 +52,22 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
llStatus.visibility = View.VISIBLE llStatus.visibility = View.VISIBLE
if (status.conversation_main) { if (status.conversation_main) {
PrefI.ipConversationMainTootBgColor(activity.pref).notZero()
val conversationMainBgColor = ?: (activity.attrColor(R.attr.colorImageButtonAccent) and 0xffffff) or 0x20000000
PrefI.ipConversationMainTootBgColor(activity.pref).notZero()
?: (activity.attrColor(R.attr.colorImageButtonAccent) and 0xffffff) or 0x20000000
this.viewRoot.setBackgroundColor(conversationMainBgColor)
} else { } else {
val c = colorBg.notZero() colorBg.notZero() ?: when (status.bookmarked) {
true -> PrefI.ipEventBgColorBookmark(App1.pref)
?: when (status.bookmarked) { false -> 0
true -> PrefI.ipEventBgColorBookmark(App1.pref) }.notZero() ?: when (status.getBackgroundColorType(accessInfo)) {
false -> 0 TootVisibility.UnlistedHome -> ItemViewHolder.toot_color_unlisted
}.notZero() TootVisibility.PrivateFollowers -> ItemViewHolder.toot_color_follower
TootVisibility.DirectSpecified -> ItemViewHolder.toot_color_direct_user
?: when (status.getBackgroundColorType(accessInfo)) { TootVisibility.DirectPrivate -> ItemViewHolder.toot_color_direct_me
TootVisibility.UnlistedHome -> ItemViewHolder.toot_color_unlisted // TODO add color setting for limited?
TootVisibility.PrivateFollowers -> ItemViewHolder.toot_color_follower TootVisibility.Limited -> ItemViewHolder.toot_color_follower
TootVisibility.DirectSpecified -> ItemViewHolder.toot_color_direct_user else -> 0
TootVisibility.DirectPrivate -> ItemViewHolder.toot_color_direct_me }.notZero()
// TODO add color setting for limited? }?.let { viewRoot.backgroundColor = it }
TootVisibility.Limited -> ItemViewHolder.toot_color_follower
else -> 0
}
if (c != 0) {
this.viewRoot.backgroundColor = c
}
}
showStatusTime(activity, tvTime, who = status.account, status = status) showStatusTime(activity, tvTime, who = status.account, status = status)
@ -88,11 +77,6 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
setAcct(tvAcct, accessInfo, who) setAcct(tvAcct, accessInfo, who)
// if(who == null) {
// tvName.text = "?"
// name_invalidator.register(null)
// ivThumbnail.setImageUrl(activity.pref, 16f, null, null)
// } else {
tvName.text = whoRef.decoded_display_name tvName.text = whoRef.decoded_display_name
nameInvalidator.register(whoRef.decoded_display_name) nameInvalidator.register(whoRef.decoded_display_name)
ivAvatar.setImageUrl( ivAvatar.setImageUrl(
@ -101,34 +85,23 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
accessInfo.supplyBaseUrl(who.avatar_static), accessInfo.supplyBaseUrl(who.avatar_static),
accessInfo.supplyBaseUrl(who.avatar) accessInfo.supplyBaseUrl(who.avatar)
) )
// }
showOpenSticker(who) showOpenSticker(who)
var content = status.decoded_content val modifiedContent = if (status.time_deleted_at > 0L) {
SpannableStringBuilder()
// ニコフレのアンケートの表示 .append('(')
val enquete = status.enquete .append(
when { activity.getString(
enquete == null -> { R.string.deleted_at,
} TootStatus.formatTime(activity, status.time_deleted_at, true)
)
enquete.pollType == TootPollsType.FriendsNico && enquete.type != TootPolls.TYPE_ENQUETE -> { )
// フレニコの投票の結果表示は普通にテキストを表示するだけでよい .append(')')
} } else {
showPoll(status) ?: status.decoded_content
else -> {
// アンケートの本文を上書きする
val question = enquete.decoded_question
if (question.isNotBlank()) content = question
showEnqueteItems(status, enquete)
}
} }
showPreviewCard(status)
// if( status.decoded_tags == null ){ // if( status.decoded_tags == null ){
// tvTags.setVisibility( View.GONE ); // tvTags.setVisibility( View.GONE );
// }else{ // }else{
@ -143,27 +116,41 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvMentions.text = status.decoded_mentions tvMentions.text = status.decoded_mentions
} }
if (status.time_deleted_at > 0L) { tvContent.text = modifiedContent
val s = SpannableStringBuilder() contentInvalidator.register(modifiedContent)
.append('(')
.append(
activity.getString(
R.string.deleted_at,
TootStatus.formatTime(activity, status.time_deleted_at, true)
)
)
.append(')')
content = s
}
tvContent.text = content activity.checkAutoCW(status, modifiedContent)
contentInvalidator.register(content)
activity.checkAutoCW(status, content)
val r = status.auto_cw val r = status.auto_cw
tvContent.minLines = r?.originalLineCount ?: -1 tvContent.minLines = r?.originalLineCount ?: -1
showPreviewCard(status)
showSpoilerTextAndContent(status)
showAttachments(status)
makeReactionsView(status)
buttonsForStatus?.bind(status, (item as? TootNotification))
showApplicationAndLanguage(status)
}
// 投票の表示
// returns modified decoded_content or null
private fun ItemViewHolder.showPoll(status: TootStatus): Spannable? {
val enquete = status.enquete
return when {
enquete == null -> null
// フレニコの投票の結果表示は普通にテキストを表示するだけでよい
enquete.pollType == TootPollsType.FriendsNico && enquete.type != TootPolls.TYPE_ENQUETE -> null
else -> {
showEnqueteItems(status, enquete)
// アンケートの本文を使ってcontentを上書きする
enquete.decoded_question.notBlank()
}
}
}
private fun ItemViewHolder.showSpoilerTextAndContent(status: TootStatus) {
val r = status.auto_cw
val decodedSpoilerText = status.decoded_spoiler_text val decodedSpoilerText = status.decoded_spoiler_text
when { when {
decodedSpoilerText.isNotEmpty() -> { decodedSpoilerText.isNotEmpty() -> {
@ -172,7 +159,7 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvContentWarning.text = status.decoded_spoiler_text tvContentWarning.text = status.decoded_spoiler_text
spoilerInvalidator.register(status.decoded_spoiler_text) spoilerInvalidator.register(status.decoded_spoiler_text)
val cwShown = ContentWarning.isShown(status, accessInfo.expand_cw) val cwShown = ContentWarning.isShown(status, accessInfo.expand_cw)
showContent(cwShown) setContentVisibility(cwShown)
} }
r?.decodedSpoilerText != null -> { r?.decodedSpoilerText != null -> {
@ -181,7 +168,7 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvContentWarning.text = r.decodedSpoilerText tvContentWarning.text = r.decodedSpoilerText
spoilerInvalidator.register(r.decodedSpoilerText) spoilerInvalidator.register(r.decodedSpoilerText)
val cwShown = ContentWarning.isShown(status, accessInfo.expand_cw) val cwShown = ContentWarning.isShown(status, accessInfo.expand_cw)
showContent(cwShown) setContentVisibility(cwShown)
} }
else -> { else -> {
@ -190,55 +177,25 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
llContents.visibility = View.VISIBLE llContents.visibility = View.VISIBLE
} }
} }
}
val mediaAttachments = status.media_attachments private fun ItemViewHolder.setContentVisibility(shown: Boolean) {
if (mediaAttachments == null || mediaAttachments.isEmpty()) { llContents.visibility = if (shown) View.VISIBLE else View.GONE
flMedia.visibility = View.GONE btnContentWarning.setText(if (shown) R.string.hide else R.string.show)
llMedia.visibility = View.GONE statusShowing?.let { status ->
btnShowMedia.visibility = View.GONE val r = status.auto_cw
} else { tvContent.minLines = r?.originalLineCount ?: -1
flMedia.visibility = View.VISIBLE if (r?.decodedSpoilerText != null) {
// 自動CWの場合はContentWarningのテキストを切り替える
// hide sensitive media tvContentWarning.text =
val defaultShown = when { if (shown) activity.getString(R.string.auto_cw_prefix) else r.decodedSpoilerText
column.hideMediaDefault -> false
accessInfo.dont_hide_nsfw -> true
else -> !status.sensitive
} }
val isShown = MediaShown.isShown(status, defaultShown)
btnShowMedia.visibility = if (!isShown) View.VISIBLE else View.GONE
llMedia.visibility = if (!isShown) View.GONE else View.VISIBLE
val sb = StringBuilder()
setMedia(mediaAttachments, sb, ivMedia1, 0)
setMedia(mediaAttachments, sb, ivMedia2, 1)
setMedia(mediaAttachments, sb, ivMedia3, 2)
setMedia(mediaAttachments, sb, ivMedia4, 3)
val m0 =
if (mediaAttachments.isEmpty()) null else mediaAttachments[0] as? TootAttachment
btnShowMedia.blurhash = m0?.blurhash
if (sb.isNotEmpty()) {
tvMediaDescription.visibility = View.VISIBLE
tvMediaDescription.text = sb
}
setIconDrawableId(
activity,
btnHideMedia,
R.drawable.ic_close,
color = contentColor,
alphaMultiplier = Styler.boostAlpha
)
} }
}
makeReactionsView(status) private fun ItemViewHolder.showApplicationAndLanguage(status: TootStatus) {
buttonsForStatus?.bind(status, (item as? TootNotification))
var sb: StringBuilder? = null var sb: StringBuilder? = null
fun prepareSb(): StringBuilder = fun prepareSb(): StringBuilder =
sb?.append(", ") ?: StringBuilder().also { sb = it } sb?.append(", ") ?: StringBuilder().also { sb = it }
@ -259,7 +216,7 @@ fun ItemViewHolder.showStatus(status: TootStatus, colorBg: Int = 0) {
tvApplication.vg(sb != null)?.text = sb tvApplication.vg(sb != null)?.text = sb
} }
fun ItemViewHolder.showOpenSticker(who: TootAccount) { private fun ItemViewHolder.showOpenSticker(who: TootAccount) {
try { try {
if (!Column.showOpenSticker) return if (!Column.showOpenSticker) return
@ -306,17 +263,47 @@ fun ItemViewHolder.showOpenSticker(who: TootAccount) {
} }
} }
fun ItemViewHolder.showContent(shown: Boolean) { private fun ItemViewHolder.showAttachments(status: TootStatus) {
llContents.visibility = if (shown) View.VISIBLE else View.GONE val mediaAttachments = status.media_attachments
btnContentWarning.setText(if (shown) R.string.hide else R.string.show) if (mediaAttachments == null || mediaAttachments.isEmpty()) {
statusShowing?.let { status -> flMedia.visibility = View.GONE
val r = status.auto_cw llMedia.visibility = View.GONE
tvContent.minLines = r?.originalLineCount ?: -1 btnShowMedia.visibility = View.GONE
if (r?.decodedSpoilerText != null) { } else {
// 自動CWの場合はContentWarningのテキストを切り替える flMedia.visibility = View.VISIBLE
tvContentWarning.text =
if (shown) activity.getString(R.string.auto_cw_prefix) else r.decodedSpoilerText // hide sensitive media
val defaultShown = when {
column.hideMediaDefault -> false
accessInfo.dont_hide_nsfw -> true
else -> !status.sensitive
} }
val isShown = MediaShown.isShown(status, defaultShown)
btnShowMedia.visibility = if (!isShown) View.VISIBLE else View.GONE
llMedia.visibility = if (!isShown) View.GONE else View.VISIBLE
val sb = StringBuilder()
setMedia(mediaAttachments, sb, ivMedia1, 0)
setMedia(mediaAttachments, sb, ivMedia2, 1)
setMedia(mediaAttachments, sb, ivMedia3, 2)
setMedia(mediaAttachments, sb, ivMedia4, 3)
val m0 =
if (mediaAttachments.isEmpty()) null else mediaAttachments[0] as? TootAttachment
btnShowMedia.blurhash = m0?.blurhash
if (sb.isNotEmpty()) {
tvMediaDescription.visibility = View.VISIBLE
tvMediaDescription.text = sb
}
setIconDrawableId(
activity,
btnHideMedia,
R.drawable.ic_close,
color = contentColor,
alphaMultiplier = Styler.boostAlpha
)
} }
} }

View File

@ -1,5 +1,6 @@
package jp.juggler.subwaytooter.action package jp.juggler.subwaytooter.action
import android.content.Context
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import jp.juggler.subwaytooter.* import jp.juggler.subwaytooter.*
import jp.juggler.subwaytooter.api.* import jp.juggler.subwaytooter.api.*
@ -29,13 +30,17 @@ private class BoostImpl(
val visibility: TootVisibility? = null, val visibility: TootVisibility? = null,
val callback: () -> Unit, val callback: () -> Unit,
) { ) {
val parser = TootParser(activity, accessInfo)
var resultStatus: TootStatus? = null
var resultUnrenoteId: EntityId? = null
// Mastodonは非公開トゥートをブーストできるのは本人だけ // Mastodonは非公開トゥートをブーストできるのは本人だけ
val isPrivateToot = accessInfo.isMastodon && private val isPrivateToot = accessInfo.isMastodon &&
statusArg.visibility == TootVisibility.PrivateFollowers statusArg.visibility == TootVisibility.PrivateFollowers
var bConfirmed = false private var bConfirmed = false
fun preCheck(): Boolean { private fun preCheck(): Boolean {
// アカウントからステータスにブースト操作を行っているなら、何もしない // アカウントからステータスにブースト操作を行っているなら、何もしない
if (activity.appState.isBusyBoost(accessInfo, statusArg)) { if (activity.appState.isBusyBoost(accessInfo, statusArg)) {
@ -52,7 +57,7 @@ private class BoostImpl(
return true return true
} }
fun confirm(): Boolean { private fun confirm(): Boolean {
if (bConfirmed) return true if (bConfirmed) return true
DlgConfirm.open( DlgConfirm.open(
activity, activity,
@ -88,178 +93,162 @@ private class BoostImpl(
return false return false
} }
private suspend fun Context.syncStatus(client: TootApiClient) =
if (!crossAccountMode.isRemote) {
// 既に自タンスのステータスがある
statusArg
} else {
val (result, status) = client.syncStatus(accessInfo, statusArg)
when {
status == null -> errorApiResult(result)
status.reblogged -> errorApiResult(getString(R.string.already_boosted))
else -> status
}
}
// ブースト結果をUIに反映させる
private fun after(result: TootApiResult?, newStatus: TootStatus?, unrenoteId: EntityId?) {
result ?: return // cancelled.
when {
// Misskeyでunrenoteに成功した
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にしない
val count = max(0, (statusArg.reblogs_count ?: 1) - 1)
for (column in activity.appState.columnList) {
column.findStatus(accessInfo.apDomain, statusArg.id) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = count
// 同アカウントならreblogged状態を変化させる
if (accessInfo == account && status.myRenoteId == unrenoteId) {
status.myRenoteId = null
status.reblogged = false
}
true
}
}
callback()
}
// 処理に成功した
newStatus != null -> {
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
val oldCount = statusArg.reblogs_count
val newCount = newStatus.reblogs_count
if (oldCount != null && newCount != null) {
if (bSet && newStatus.reblogged && newCount <= oldCount) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
newStatus.reblogs_count = oldCount + 1
} else if (!bSet && !newStatus.reblogged && newCount >= oldCount) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはしない
newStatus.reblogs_count = if (oldCount < 1) 0 else oldCount - 1
}
}
for (column in activity.appState.columnList) {
column.findStatus(accessInfo.apDomain, newStatus.id) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = newStatus.reblogs_count
if (accessInfo == account) {
// 同アカウントならreblog状態を変化させる
when {
accessInfo.isMastodon ->
status.reblogged = newStatus.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = newStatus.myRenoteId
status.reblogged = true
}
// Misskey のunrenote時はここを通らない
}
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
}
suspend fun boostApi(client: TootApiClient, targetStatus: TootStatus): TootApiResult? =
if (accessInfo.isMisskey) {
if (!bSet) {
val myRenoteId = targetStatus.myRenoteId ?: errorApiResult("missing renote id.")
client.request(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
)?.also {
if (it.response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
} else {
client.request(
"/api/notes/create",
accessInfo.putMisskeyApiToken().apply {
put("renoteId", targetStatus.id.toString())
}.toPostRequestBuilder()
)?.also { result ->
val jsonObject = result.jsonObject
if (jsonObject != null) {
val outerStatus = parser.status(jsonObject.jsonObject("createdNote") ?: jsonObject)
val innerStatus = outerStatus?.reblog ?: outerStatus
if (outerStatus != null && innerStatus != null && outerStatus != innerStatus) {
innerStatus.myRenoteId = outerStatus.id
innerStatus.reblogged = true
}
// renoteそのものではなくrenoteされた元noteが欲しい
resultStatus = innerStatus
}
}
}
} else {
val b = JsonObject().apply {
if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
client.request(
"/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}",
b
)?.also { result ->
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
val s = parser.status(result.jsonObject)
resultStatus = s?.reblog ?: s
}
}
fun run() { fun run() {
if (!preCheck()) return if (!preCheck()) return
if (!confirm()) return if (!confirm()) return
activity.appState.setBusyBoost(accessInfo, statusArg)
// ブースト表示を更新中にする // ブースト表示を更新中にする
activity.appState.setBusyBoost(accessInfo, statusArg)
activity.showColumnMatchAccount(accessInfo) activity.showColumnMatchAccount(accessInfo)
// misskeyは非公開トゥートをブーストできないっぽい
launchMain { launchMain {
var resultStatus: TootStatus? = null
var resultUnrenoteId: EntityId? = null
val result = activity.runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client -> val result = activity.runApiTask(accessInfo, progressStyle = ApiTask.PROGRESS_NONE) { client ->
try {
val parser = TootParser(this, accessInfo) val targetStatus = syncStatus(client)
boostApi(client, targetStatus)
val targetStatus = if (crossAccountMode.isRemote) { } catch (ex: TootApiResultException) {
val (result, status) = client.syncStatus(accessInfo, statusArg) ex.result
if (status == null) return@runApiTask result
if (status.reblogged) {
return@runApiTask TootApiResult(getString(R.string.already_boosted))
}
status
} else {
// 既に自タンスのステータスがある
statusArg
}
if (accessInfo.isMisskey) {
if (!bSet) {
val myRenoteId = targetStatus.myRenoteId
?: return@runApiTask TootApiResult("missing renote id.")
client.request(
"/api/notes/delete",
accessInfo.putMisskeyApiToken().apply {
put("noteId", myRenoteId.toString())
put("renoteId", targetStatus.id.toString())
}
.toPostRequestBuilder()
)
?.also {
if (it.response?.code == 204) {
resultUnrenoteId = myRenoteId
}
}
} else {
client.request(
"/api/notes/create",
accessInfo.putMisskeyApiToken().apply {
put("renoteId", targetStatus.id.toString())
}
.toPostRequestBuilder()
)
?.also { result ->
val jsonObject = result.jsonObject
if (jsonObject != null) {
val outerStatus = parser.status(
jsonObject.jsonObject("createdNote")
?: jsonObject
)
val innerStatus = outerStatus?.reblog ?: outerStatus
if (outerStatus != null && innerStatus != null && outerStatus != innerStatus) {
innerStatus.myRenoteId = outerStatus.id
innerStatus.reblogged = true
}
// renoteそのものではなくrenoteされた元noteが欲しい
resultStatus = innerStatus
}
}
}
} else {
val b = JsonObject().apply {
if (visibility != null) put("visibility", visibility.strMastodon)
}.toPostRequestBuilder()
client.request(
"/api/v1/statuses/${targetStatus.id}/${if (bSet) "reblog" else "unreblog"}",
b
)?.also { result ->
// reblogはreblogを表すStatusを返す
// unreblogはreblogしたStatusを返す
val s = parser.status(result.jsonObject)
resultStatus = s?.reblog ?: s
}
} }
} }
// 更新中状態をリセット
activity.appState.resetBusyBoost(accessInfo, statusArg) activity.appState.resetBusyBoost(accessInfo, statusArg)
// カラムデータの書き換え
if (result != null) { after(result, resultStatus, resultUnrenoteId)
val unrenoteId = resultUnrenoteId // result == null の場合でも更新中表示の解除が必要になる
val newStatus = resultStatus
when {
// Misskeyでunrenoteに成功した
unrenoteId != null -> {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
val count = max(0, (statusArg.reblogs_count ?: 1) - 1)
for (column in activity.appState.columnList) {
column.findStatus(
accessInfo.apDomain,
statusArg.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = count
// 同アカウントならreblogged状態を変化させる
if (accessInfo == account && status.myRenoteId == unrenoteId) {
status.myRenoteId = null
status.reblogged = false
}
true
}
}
callback()
}
// 処理に成功した
newStatus != null -> {
// カウント数は遅延があるみたいなので、恣意的に表示を変更する
// ブーストカウント数を加工する
val oldCount = statusArg.reblogs_count
val newCount = newStatus.reblogs_count
if (oldCount != null && newCount != null) {
if (bSet && newStatus.reblogged && newCount <= oldCount) {
// 星をつけたのにカウントが上がらないのは違和感あるので、表示をいじる
newStatus.reblogs_count = oldCount + 1
} else if (!bSet && !newStatus.reblogged && newCount >= oldCount) {
// 星を外したのにカウントが下がらないのは違和感あるので、表示をいじる
// 0未満にはならない
newStatus.reblogs_count = if (oldCount < 1) 0 else oldCount - 1
}
}
for (column in activity.appState.columnList) {
column.findStatus(
accessInfo.apDomain,
newStatus.id
) { account, status ->
// 同タンス別アカウントでもカウントは変化する
status.reblogs_count = newStatus.reblogs_count
if (accessInfo == account) {
// 同アカウントならreblog状態を変化させる
when {
accessInfo.isMastodon ->
status.reblogged = newStatus.reblogged
bSet && status.myRenoteId == null -> {
status.myRenoteId = newStatus.myRenoteId
status.reblogged = true
}
}
// Misskey のunrenote時はここを通らない
}
true
}
}
callback()
}
else -> activity.showToast(true, result.error)
}
}
// 結果に関わらず、更新中状態から復帰させる
activity.showColumnMatchAccount(accessInfo) activity.showColumnMatchAccount(accessInfo)
} }
} }

View File

@ -0,0 +1,10 @@
package jp.juggler.subwaytooter.api
import java.lang.Exception
class TootApiResultException(val result: TootApiResult?) : Exception(result?.error ?: "cancelled.") {
constructor(error: String) : this(TootApiResult(error))
}
fun errorApiResult(result: TootApiResult?):Nothing = throw TootApiResultException(result)
fun errorApiResult(error:String):Nothing = throw TootApiResultException(error)

View File

@ -3,9 +3,10 @@ package jp.juggler.subwaytooter.api.entity
import android.content.ContentValues import android.content.ContentValues
import android.content.Intent import android.content.Intent
import android.database.Cursor import android.database.Cursor
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import jp.juggler.util.* import jp.juggler.util.JsonObject
import jp.juggler.util.getStringOrNull
import jp.juggler.util.notZero
import kotlinx.serialization.KSerializer import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@ -109,9 +110,8 @@ object EntityIdSerializer : KSerializer<EntityId> {
PrimitiveSerialDescriptor("EntityId", PrimitiveKind.STRING) PrimitiveSerialDescriptor("EntityId", PrimitiveKind.STRING)
override fun serialize(encoder: Encoder, value: EntityId) = override fun serialize(encoder: Encoder, value: EntityId) =
encoder.encodeString(value.toString() ) encoder.encodeString(value.toString())
override fun deserialize(decoder: Decoder): EntityId = override fun deserialize(decoder: Decoder): EntityId =
EntityId(decoder.decodeString()) EntityId(decoder.decodeString())
} }

View File

@ -46,15 +46,15 @@ class TootScheduled(parser: TootParser, val src: JsonObject) : TimelineItem() {
// 投稿画面の復元時に、IDだけでもないと困る // 投稿画面の復元時に、IDだけでもないと困る
fun encodeSimple() = jsonObject { fun encodeSimple() = jsonObject {
put("id",id.toString()) put("id", id.toString())
put("scheduled_at",scheduledAt) put("scheduled_at", scheduledAt)
// SKIP: put("media_attachments",mediaAttachments?.map{ it.}) // SKIP: put("media_attachments",mediaAttachments?.map{ it.})
put("params", jsonObject { put("params", jsonObject {
put("text",text) put("text", text)
put("visibility",visibility.strMastodon) put("visibility", visibility.strMastodon)
put("spoiler_text",spoilerText) put("spoiler_text", spoilerText)
put("in_reply_to_id",inReplyToId) put("in_reply_to_id", inReplyToId)
put("sensitive",sensitive) put("sensitive", sensitive)
}) })
} }
} }

View File

@ -0,0 +1,95 @@
package jp.juggler.subwaytooter.api.finder
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.JsonArray
import jp.juggler.util.JsonObject
val nullArrayFinder: (JsonObject) -> JsonArray? =
{ null }
val misskeyArrayFinderUsers = { it: JsonObject ->
it.jsonArray("users")
}
////////////////////////////////////////////////////////////////////////////////
// account list parser
val defaultAccountListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> parser.accountList(jsonArray) }
private fun misskeyUnwrapRelationAccount(parser: TootParser, srcList: JsonArray, key: String) =
srcList.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> TootAccountRef.mayNull(parser, parser.account(it.jsonObject(key)))
?.apply { _orderId = relationId }
}
}
val misskey11FollowingParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "followee") }
val misskey11FollowersParser: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserFollowRequest: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "follower") }
val misskeyCustomParserMutes: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "mutee") }
val misskeyCustomParserBlocks: (TootParser, JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray -> misskeyUnwrapRelationAccount(parser, jsonArray, "blockee") }
////////////////////////////////////////////////////////////////////////////////
// status list parser
val defaultStatusListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootStatus> =
{ parser, jsonArray -> parser.statusList(jsonArray) }
val misskeyCustomParserFavorites: (TootParser, JsonArray) -> List<TootStatus> =
{ parser, jsonArray ->
jsonArray.objectList().mapNotNull {
when (val relationId = EntityId.mayNull(it.string("id"))) {
null -> null
else -> parser.status(it.jsonObject("note"))?.apply {
favourited = true
_orderId = relationId
}
}
}
}
////////////////////////////////////////////////////////////////////////////////
// notification list parser
val defaultNotificationListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootNotification> =
{ parser, jsonArray -> parser.notificationList(jsonArray) }
val defaultDomainBlockListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootDomainBlock> =
{ _, jsonArray -> TootDomainBlock.parseList(jsonArray) }
val defaultReportListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootReport> =
{ _, jsonArray -> parseList(::TootReport, jsonArray) }
val defaultConversationSummaryListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootConversationSummary> =
{ parser, jsonArray -> parseList(::TootConversationSummary, parser, jsonArray) }
///////////////////////////////////////////////////////////////////////
val mastodonFollowSuggestion2ListParser: (parser: TootParser, jsonArray: JsonArray) -> List<TootAccountRef> =
{ parser, jsonArray ->
TootAccountRef.wrapList(parser,
jsonArray.objectList().mapNotNull {
parser.account(it.jsonObject("account"))?.also { a ->
SuggestionSource.set(
(parser.linkHelper as? SavedAccount)?.db_id,
a.acct,
it.string("source")
)
}
}
)
}

View File

@ -1,8 +1,6 @@
package jp.juggler.subwaytooter.emoji package jp.juggler.subwaytooter.emoji
import android.content.Context import android.content.Context
import android.util.Log
import java.io.EOFException
import java.io.InputStream import java.io.InputStream
import java.util.* import java.util.*
@ -21,135 +19,7 @@ object EmojiMap {
///////////////////////////////////////////////////////////////// /////////////////////////////////////////////////////////////////
private fun readStream(appContext: Context, inStream: InputStream) { private fun readStream(appContext: Context, inStream: InputStream) {
val assetManager = appContext.assets!! EmojiMapLoader(appContext, this).readStream(inStream)
val resources = appContext.resources!!
val packageName = appContext.packageName!!
fun getDrawableId(name: String) =
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
val categoryNameMap = HashMap<String, EmojiCategory>().apply {
EmojiCategory.values().forEach { put(it.name, it) }
}
// 素の数字とcopyright,registered, trademark は絵文字にしない
fun isIgnored(code: String): Boolean {
val c = code[0].code
return code.length == 1 && c <= 0xae
}
fun addCode(emoji: UnicodeEmoji, code: String) {
if (isIgnored(code)) return
unicodeMap[code] = emoji
unicodeTrie.append(code, 0, emoji)
}
fun addName(emoji: UnicodeEmoji, name: String) {
shortNameMap[name] = emoji
shortNameList.add(name)
}
val reComment = """\s*//.*""".toRegex()
val reLineHeader = """\A(\w+):""".toRegex()
val assetsSet = assetManager.list("")!!.toSet()
var lastEmoji: UnicodeEmoji? = null
var lastCategory: EmojiCategory? = null
var lno = 0
fun readEmojiDataLine(rawLine: String) {
++lno
var line = rawLine.replace(reComment, "").trim()
val head = reLineHeader.find(line)?.groupValues?.elementAtOrNull(1)
?: error("missing line header. line=$lno $line")
line = line.substring(head.length + 1)
try {
when (head) {
"svg" -> {
if (!assetsSet.contains(line)) error("missing assets.")
lastEmoji = UnicodeEmoji(assetsName = line)
}
"drawable" -> {
val drawableId = getDrawableId(line) ?: error("missing drawable.")
lastEmoji = UnicodeEmoji(drawableId = drawableId)
}
"un" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
emoji.unifiedCode = line
}
"u" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
}
"sn" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
emoji.unifiedName = line
}
"s" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
}
"t" -> {
val cols = line.split(",", limit = 3)
if (cols.size != 3) error("invalid tone spec. line=$lno $line")
val parent = unicodeMap[cols[0]]
?: error("missing tone parent. line=$lno $line")
val toneCode = cols[1].takeIf { it.isNotEmpty() }
?: error("missing tone code. line=$lno $line")
val child = unicodeMap[cols[2]]
?: error("missing tone child. line=$lno $line")
parent.toneChildren.add(Pair(toneCode, child))
child.toneParent = parent
}
"cn" -> {
lastCategory = categoryNameMap[line]
?: error("missing category name.")
}
"c" -> {
val category = lastCategory
?: error("missing lastCategory.")
val emoji = unicodeMap[line] ?: error("missing emoji.")
// if (emoji == null) {
// Log.w("SubwayTooter", "missing emoji. lno=$lno line=$rawLine")
// } else
if (!category.emojiList.contains(emoji)) {
category.emojiList.add(emoji)
}
}
else -> error("unknown header $head")
}
} catch (ex: Throwable) {
Log.e("SubwayTooter", "EmojiMap load error.", ex)
error("EmojiMap load error: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine")
}
}
val lineFeed = 0x0a.toByte()
val buffer = ByteArray(4096)
var used = inStream.read(buffer, 0, buffer.size)
if (used <= 0) throw EOFException("unexpected EOF")
while (true) {
var lineStart = 0
while (lineStart < used) {
var i = lineStart
while (i < used && buffer[i] != lineFeed) ++i
if (i >= used) break
if (i > lineStart) {
val line = String(buffer, lineStart, i - lineStart, Charsets.UTF_8)
readEmojiDataLine(line)
}
lineStart = i + 1
}
buffer.copyInto(buffer, 0, lineStart, used)
used -= lineStart
val nRead = inStream.read(buffer, used, buffer.size - used)
if (nRead <= 0) {
if (used > 0) throw EOFException("unexpected EOF")
break
}
used += nRead
}
} }
fun load(appContext: Context) { fun load(appContext: Context) {

View File

@ -0,0 +1,167 @@
package jp.juggler.subwaytooter.emoji
import android.content.Context
import jp.juggler.util.LogCategory
import jp.juggler.util.errorEx
import java.io.EOFException
import java.io.InputStream
import java.util.HashMap
class EmojiMapLoader(
appContext: Context,
private val dst: EmojiMap,
) {
// このクラスは起動時に1回だけ使うため、companion objectに永続的に何か保持することはない
private val log = LogCategory("EmojiMapLoader")
private val reComment = """\s*//.*""".toRegex()
private val reLineHeader = """\A(\w+):""".toRegex()
private val packageName = appContext.packageName!!
private val assetsSet = appContext.assets.list("")!!.toSet()
private val resources = appContext.resources!!
private val categoryNameMap = HashMap<String, EmojiCategory>().apply {
EmojiCategory.values().forEach { put(it.name, it) }
}
private var lastEmoji: UnicodeEmoji? = null
private var lastCategory: EmojiCategory? = null
private fun getDrawableId(name: String) =
resources.getIdentifier(name, "drawable", packageName).takeIf { it != 0 }
// 素の数字とcopyright,registered, trademark は絵文字にしない
private fun isIgnored(code: String): Boolean {
val c = code[0].code
return code.length == 1 && c <= 0xae
}
private fun addCode(emoji: UnicodeEmoji, code: String) {
if (isIgnored(code)) return
dst.unicodeMap[code] = emoji
dst.unicodeTrie.append(code, 0, emoji)
}
private fun addName(emoji: UnicodeEmoji, name: String) {
dst.shortNameMap[name] = emoji
dst.shortNameList.add(name)
}
private fun readEmojiDataLine(lno: Int, rawLine: String) {
var line = rawLine.replace(reComment, "").trim()
val head = reLineHeader.find(line)?.groupValues?.elementAtOrNull(1)
?: error("missing line header. line=$lno $line")
line = line.substring(head.length + 1)
try {
when (head) {
"svg" -> {
if (!assetsSet.contains(line)) error("missing assets.")
lastEmoji = UnicodeEmoji(assetsName = line)
}
"drawable" -> {
val drawableId = getDrawableId(line) ?: error("missing drawable.")
lastEmoji = UnicodeEmoji(drawableId = drawableId)
}
"un" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
emoji.unifiedCode = line
}
"u" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addCode(emoji, line)
}
"sn" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
emoji.unifiedName = line
}
"s" -> {
val emoji = lastEmoji ?: error("missing lastEmoji.")
addName(emoji, line)
}
"t" -> {
val cols = line.split(",", limit = 3)
if (cols.size != 3) error("invalid tone spec. line=$lno $line")
val parent = dst.unicodeMap[cols[0]]
?: error("missing tone parent. line=$lno $line")
val toneCode = cols[1].takeIf { it.isNotEmpty() }
?: error("missing tone code. line=$lno $line")
val child = dst.unicodeMap[cols[2]]
?: error("missing tone child. line=$lno $line")
parent.toneChildren.add(Pair(toneCode, child))
child.toneParent = parent
}
"cn" -> {
lastCategory = categoryNameMap[line]
?: error("missing category name.")
}
"c" -> {
val category = lastCategory
?: error("missing lastCategory.")
val emoji = dst.unicodeMap[line] ?: error("missing emoji.")
// if (emoji == null) {
// Log.w("SubwayTooter", "missing emoji. lno=$lno line=$rawLine")
// } else
if (!category.emojiList.contains(emoji)) {
category.emojiList.add(emoji)
}
}
else -> error("unknown header $head")
}
} catch (ex: Throwable) {
log.e(ex, "readEmojiDataLine: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine")
// 行番号の情報をつけて投げ直す
errorEx(ex, "readEmojiDataLine: ${ex.javaClass.simpleName} ${ex.message} lno=$lno line=$rawLine")
}
}
private fun ByteArray.indexOf(key: Byte, start: Int = 0): Int? {
var i = start
val end = this.size
while (i < end) {
if (this[i] == key) return i
++i
}
return null
}
private fun InputStream.eachLine(block: (Int, String) -> Unit) {
val lineFeed = 0x0a.toByte()
val buffer = ByteArray(4096)
// バッファに読む
var end = read(buffer, 0, buffer.size)
if (end <= 0) throw EOFException("unexpected EOF")
var lno = 0
while (true) {
var lineStart = 0
while (lineStart < end) {
// 行末記号を見つける
val feedPos = buffer.indexOf(lineFeed, lineStart) ?: break
++lno
if (feedPos > lineStart) {
// 1行分をUTF-8デコードして処理する
val line = String(buffer, lineStart, feedPos - lineStart, Charsets.UTF_8)
block(lno, line)
}
lineStart = feedPos + 1
}
// 最後の行末より後のデータをバッファ先頭に移動する
buffer.copyInto(buffer, 0, lineStart, end)
end -= lineStart
// ストリームから継ぎ足す
val nRead = read(buffer, end, buffer.size - end)
if (nRead <= 0) {
if (end > 0) throw EOFException("unexpected EOF")
break
}
end += nRead
}
}
fun readStream(inStream: InputStream) {
inStream.eachLine { lno, line -> readEmojiDataLine(lno, line) }
}
}

View File

@ -1,10 +1,10 @@
package jp.juggler.subwaytooter.notification package jp.juggler.subwaytooter.notification
import android.annotation.TargetApi
import android.app.PendingIntent import android.app.PendingIntent
import android.content.Intent import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.service.notification.StatusBarNotification
import androidx.core.app.NotificationCompat import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import jp.juggler.subwaytooter.ActCallback import jp.juggler.subwaytooter.ActCallback
@ -463,215 +463,170 @@ class TaskRunner(
} }
fun updateNotification() { fun updateNotification() {
val notificationTag = when (trackingName) { val notificationTag = when (trackingName) {
"" -> "${account.db_id}/_" "" -> "${account.db_id}/_"
else -> "${account.db_id}/$trackingName" else -> "${account.db_id}/$trackingName"
} }
val nt = NotificationTracking.load(account.acct.pretty, account.db_id, trackingName) val nt = NotificationTracking.load(account.acct.pretty, account.db_id, trackingName)
val dataList = dstListData when (val first = dstListData.firstOrNull()) {
val first = dataList.firstOrNull() null -> {
if (first == null) { log.d("showNotification[${account.acct.pretty}/$notificationTag] cancel notification.")
log.d("showNotification[${account.acct.pretty}/$notificationTag] cancel notification.") removeNotification(notificationTag)
if (Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref)) { }
notificationManager.activeNotifications?.forEach { else -> {
if (it != null && when {
it.id == PollingWorker.NOTIFICATION_ID && // 先頭にあるデータが同じなら、通知を更新しない
it.tag.startsWith("$notificationTag/") // このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
) { first.notification.time_created_at == nt.post_time && first.notification.id == nt.post_id ->
log.d("cancel: ${it.tag} context=${account.acct.pretty} $notificationTag") log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
notificationManager.cancel(it.tag, PollingWorker.NOTIFICATION_ID)
Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref) -> {
updateNotificationDivided(notificationTag, nt)
nt.updatePost(first.notification.id, first.notification.time_created_at)
}
else -> {
updateNotificationMerged(notificationTag, first)
nt.updatePost(first.notification.id, first.notification.time_created_at)
} }
} }
} else {
notificationManager.cancel(notificationTag, PollingWorker.NOTIFICATION_ID)
} }
return
} }
}
private fun removeNotification(notificationTag: String) {
if (Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref)) {
notificationManager.activeNotifications?.filterNotNull()?.filter {
it.id == PollingWorker.NOTIFICATION_ID && it.tag.startsWith("$notificationTag/")
}?.forEach {
log.d("cancel: ${it.tag} context=${account.acct.pretty} $notificationTag")
notificationManager.cancel(it.tag, PollingWorker.NOTIFICATION_ID)
}
} else {
notificationManager.cancel(notificationTag, PollingWorker.NOTIFICATION_ID)
}
}
@TargetApi(23)
private fun updateNotificationDivided(notificationTag: String, nt: NotificationTracking) {
log.d("updateNotificationDivided[${account.acct.pretty}] creating notification(1)")
val activeNotificationMap = notificationManager.activeNotifications?.filterNotNull()?.filter {
it.id == PollingWorker.NOTIFICATION_ID && it.tag.startsWith("$notificationTag/")
}?.map { Pair(it.tag, it) }?.toMutableMap() ?: mutableMapOf()
val lastPostTime = nt.post_time val lastPostTime = nt.post_time
val lastPostId = nt.post_id val lastPostId = nt.post_id
if (first.notification.time_created_at == lastPostTime &&
first.notification.id == lastPostId
) {
// 先頭にあるデータが同じなら、通知を更新しない
// このマーカーは端末再起動時にリセットされるので、再起動後は通知が出るはず
log.d("showNotification[${account.acct.pretty}] id=${first.notification.id} is already shown.")
return
}
if (Build.VERSION.SDK_INT >= 23 && PrefB.bpDivideNotification(pref)) { for (item in dstListData.reversed()) {
val activeNotificationMap = HashMap<String, StatusBarNotification>().apply { val itemTag = "$notificationTag/${item.notification.id}"
notificationManager.activeNotifications?.forEach {
if (it != null && if (lastPostId != null &&
it.id == PollingWorker.NOTIFICATION_ID && item.notification.time_created_at <= lastPostTime &&
it.tag.startsWith("$notificationTag/") item.notification.id <= lastPostId
) { ) {
put(it.tag, it) // 掲載済みデータより古い通知は再表示しない
} log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
} continue
} }
for (item in dstListData.reversed()) {
val itemTag = "$notificationTag/${item.notification.id}"
if (lastPostId != null && // ignore if already showing
item.notification.time_created_at <= lastPostTime && if (activeNotificationMap.remove(itemTag) != null) {
item.notification.id <= lastPostId log.d("ignore $itemTag is in activeNotificationMap")
) { continue
// 掲載済みデータより古い通知は再表示しない
log.d("ignore $itemTag ${item.notification.time_created_at} <= $lastPostTime && ${item.notification.id} <= $lastPostId")
continue
}
// ignore if already showing
if (activeNotificationMap.remove(itemTag) != null) {
log.d("ignore $itemTag is in activeNotificationMap")
continue
}
createNotification(
itemTag,
notificationId = item.notification.id.toString()
) { builder ->
builder.setWhen(item.notification.time_created_at)
val summary = item.getNotificationLine()
builder.setContentTitle(summary)
val content = item.notification.status?.decoded_content?.notEmpty()
if (content != null) {
builder.setStyle(
NotificationCompat.BigTextStyle()
.setBigContentTitle(summary)
.setSummaryText(item.accessInfo.acct.pretty)
.bigText(content)
)
} else {
builder.setContentText(item.accessInfo.acct.pretty)
}
if (Build.VERSION.SDK_INT < 26) {
var iv = 0
if (PrefB.bpNotificationSound(pref)) {
var soundUri: Uri? = null
try {
val whoAcct = account.getFullAcct(item.notification.account)
soundUri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (soundUri == null) {
soundUri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (soundUri != null) {
try {
builder.setSound(soundUri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (PrefB.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (PrefB.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
}
}
} }
// リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
} else {
log.d("showNotification[${account.acct.pretty}] creating notification(1)")
createNotification(notificationTag) { builder ->
builder.setWhen(first.notification.time_created_at) createNotification(itemTag, notificationId = item.notification.id.toString()) { builder ->
builder.setWhen(item.notification.time_created_at)
var a = first.getNotificationLine() val summary = item.getNotificationLine()
builder.setContentTitle(summary)
if (dataList.size == 1) { when (val content = item.notification.status?.decoded_content?.notEmpty()) {
builder.setContentTitle(a) null -> builder.setContentText(item.accessInfo.acct.pretty)
builder.setContentText(account.acct.pretty) else -> {
} else { val style = NotificationCompat.BigTextStyle()
val header = .setBigContentTitle(summary)
context.getString(R.string.notification_count, dataList.size) .setSummaryText(item.accessInfo.acct.pretty)
builder.setContentTitle(header) .bigText(content)
.setContentText(a) builder.setStyle(style)
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(header)
.setSummaryText(account.acct.pretty)
for (i in 0..4) {
if (i >= dataList.size) break
val item = dataList[i]
a = item.getNotificationLine()
style.addLine(a)
} }
builder.setStyle(style)
}
if (Build.VERSION.SDK_INT < 26) {
var iv = 0
if (PrefB.bpNotificationSound(pref)) {
var soundUri: Uri? = null
try {
val whoAcct =
account.getFullAcct(first.notification.account)
soundUri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (soundUri == null) {
soundUri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (soundUri != null) {
try {
builder.setSound(soundUri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (PrefB.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (PrefB.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
} }
if (Build.VERSION.SDK_INT < 26) setNotificationSound25(builder, item)
} }
} }
nt.updatePost(first.notification.id, first.notification.time_created_at) // リストにない通知は消さない。ある通知をユーザが指で削除した際に他の通知が残ってほしい場合がある
}
private fun updateNotificationMerged(
notificationTag: String,
first: NotificationData,
) {
log.d("updateNotificationMerged[${account.acct.pretty}] creating notification(1)")
createNotification(notificationTag) { builder ->
builder.setWhen(first.notification.time_created_at)
val a = first.getNotificationLine()
val dataList = dstListData
if (dataList.size == 1) {
builder.setContentTitle(a)
builder.setContentText(account.acct.pretty)
} else {
val header = context.getString(R.string.notification_count, dataList.size)
builder.setContentTitle(header).setContentText(a)
val style = NotificationCompat.InboxStyle()
.setBigContentTitle(header)
.setSummaryText(account.acct.pretty)
for (i in 0 until min(4, dataList.size)) {
style.addLine(dataList[i].getNotificationLine())
}
builder.setStyle(style)
}
if (Build.VERSION.SDK_INT < 26) setNotificationSound25(builder, first)
}
}
// Android 8 未満ではチャネルではなく通知に個別にスタイルを設定する
@TargetApi(25)
private fun setNotificationSound25(builder: NotificationCompat.Builder, item: NotificationData) {
var iv = 0
if (PrefB.bpNotificationSound(pref)) {
var soundUri: Uri? = null
try {
val whoAcct = account.getFullAcct(item.notification.account)
soundUri = AcctColor.getNotificationSound(whoAcct).mayUri()
} catch (ex: Throwable) {
log.trace(ex)
}
if (soundUri == null) {
soundUri = account.sound_uri.mayUri()
}
var bSoundSet = false
if (soundUri != null) {
try {
builder.setSound(soundUri)
bSoundSet = true
} catch (ex: Throwable) {
log.trace(ex)
}
}
if (!bSoundSet) {
iv = iv or NotificationCompat.DEFAULT_SOUND
}
}
if (PrefB.bpNotificationVibration(pref)) {
iv = iv or NotificationCompat.DEFAULT_VIBRATE
}
if (PrefB.bpNotificationLED(pref)) {
iv = iv or NotificationCompat.DEFAULT_LIGHTS
}
builder.setDefaults(iv)
} }
private fun createNotification( private fun createNotification(
@ -701,41 +656,35 @@ class TaskRunner(
"type" to trackingType.str, "type" to trackingType.str,
"notificationId" to notificationId "notificationId" to notificationId
).mapNotNull { ).mapNotNull {
val second = it.second when (val second = it.second) {
if (second == null) { null -> null
null else -> "${it.first.encodePercent()}=${second.encodePercent()}"
} else {
"${it.first.encodePercent()}=${second.encodePercent()}"
} }
}.joinToString("&") }.joinToString("&")
setContentIntent( val flag = PendingIntent.FLAG_UPDATE_CURRENT or
PendingIntent.getActivity( (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
context,
257,
Intent(context, ActCallback::class.java).apply {
data =
"subwaytooter://notification_click/?$params".toUri()
// FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない PendingIntent.getActivity(
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) context,
}, 257,
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0) Intent(context, ActCallback::class.java).apply {
) data = "subwaytooter://notification_click/?$params".toUri()
) // FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY を付与してはいけない
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
},
flag
)?.let { setContentIntent(it) }
setDeleteIntent( PendingIntent.getBroadcast(
PendingIntent.getBroadcast( context,
context, 257,
257, Intent(context, EventReceiver::class.java).apply {
Intent(context, EventReceiver::class.java).apply { action = EventReceiver.ACTION_NOTIFICATION_DELETE
action = EventReceiver.ACTION_NOTIFICATION_DELETE data = "subwaytooter://notification_delete/?$params".toUri()
data = },
"subwaytooter://notification_delete/?$params".toUri() flag
}, )?.let { setDeleteIntent(it) }
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= 23) PendingIntent.FLAG_IMMUTABLE else 0)
)
)
setAutoCancel(true) setAutoCancel(true)
@ -752,16 +701,10 @@ class TaskRunner(
} }
log.d("showNotification[${account.acct.pretty}] creating notification(3)") log.d("showNotification[${account.acct.pretty}] creating notification(3)")
setContent(builder) setContent(builder)
log.d("showNotification[${account.acct.pretty}] set notification...") log.d("showNotification[${account.acct.pretty}] set notification...")
notificationManager.notify(notificationTag, PollingWorker.NOTIFICATION_ID, builder.build())
notificationManager.notify(
notificationTag,
PollingWorker.NOTIFICATION_ID,
builder.build()
)
} }
} }
} }

View File

@ -7,11 +7,8 @@ import jp.juggler.subwaytooter.App1
import jp.juggler.subwaytooter.PrefB import jp.juggler.subwaytooter.PrefB
import jp.juggler.subwaytooter.R import jp.juggler.subwaytooter.R
import jp.juggler.subwaytooter.Styler import jp.juggler.subwaytooter.Styler
import jp.juggler.subwaytooter.api.TootApiClient import jp.juggler.subwaytooter.api.*
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.* import jp.juggler.subwaytooter.api.entity.*
import jp.juggler.subwaytooter.api.runApiTask
import jp.juggler.subwaytooter.dialog.DlgConfirm import jp.juggler.subwaytooter.dialog.DlgConfirm
import jp.juggler.subwaytooter.emoji.CustomEmoji import jp.juggler.subwaytooter.emoji.CustomEmoji
import jp.juggler.subwaytooter.span.MyClickableSpan import jp.juggler.subwaytooter.span.MyClickableSpan
@ -23,12 +20,10 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody import okhttp3.RequestBody.Companion.toRequestBody
import java.lang.Exception
import java.lang.ref.WeakReference import java.lang.ref.WeakReference
import java.util.* import java.util.*
interface PostCompleteCallback { interface PostCompleteCallback {
fun onPostComplete(targetAccount: SavedAccount, status: TootStatus) fun onPostComplete(targetAccount: SavedAccount, status: TootStatus)
fun onScheduledPostComplete(targetAccount: SavedAccount) fun onScheduledPostComplete(targetAccount: SavedAccount)
} }
@ -247,10 +242,6 @@ class PostImpl(
} }
} }
private class TootApiResultException(val result: TootApiResult?) : Exception(result?.error ?: "cancelled.") {
constructor(error: String) : this(TootApiResult(error))
}
private suspend fun getWebVisibility( private suspend fun getWebVisibility(
client: TootApiClient, client: TootApiClient,
parser: TootParser, parser: TootParser,
@ -261,10 +252,10 @@ class PostImpl(
val r2 = getCredential(client, parser) val r2 = getCredential(client, parser)
val credentialTmp = resultCredentialTmp val credentialTmp = resultCredentialTmp
?: throw TootApiResultException(r2) ?: errorApiResult(r2)
val privacy = credentialTmp.source?.privacy val privacy = credentialTmp.source?.privacy
?: throw TootApiResultException(activity.getString(R.string.cant_get_web_setting_visibility)) ?: errorApiResult(activity.getString(R.string.cant_get_web_setting_visibility))
return TootVisibility.parseMastodon(privacy) return TootVisibility.parseMastodon(privacy)
// may null, not error // may null, not error
@ -278,7 +269,7 @@ class PostImpl(
) { ) {
if (actual != extra || checkFun(instance)) return if (actual != extra || checkFun(instance)) return
val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, extra) val strVisibility = Styler.getVisibilityString(activity, account.isMisskey, extra)
throw TootApiResultException(activity.getString(R.string.server_has_no_support_of_visibility, strVisibility)) errorApiResult(activity.getString(R.string.server_has_no_support_of_visibility, strVisibility))
} }
private suspend fun checkVisibility( private suspend fun checkVisibility(
@ -391,7 +382,7 @@ class PostImpl(
} }
.toPostRequestBuilder() .toPostRequestBuilder()
) )
if (r == null || r.error != null) throw TootApiResultException(r) if (r == null || r.error != null) errorApiResult(r)
} }
} }
if (array.isNotEmpty()) json["mediaIds"] = array if (array.isNotEmpty()) json["mediaIds"] = array
@ -449,7 +440,7 @@ class PostImpl(
if (scheduledAt != 0L) { if (scheduledAt != 0L) {
if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) { if (!instance.versionGE(TootInstance.VERSION_2_7_0_rc1)) {
throw TootApiResultException(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0)) errorApiResult(activity.getString(R.string.scheduled_status_requires_mastodon_2_7_0))
} }
// UTCの日時を渡す // UTCの日時を渡す
val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC")) val c = GregorianCalendar.getInstance(TimeZone.getTimeZone("UTC"))

View File

@ -145,6 +145,7 @@ class ProgressResponseBody private constructor(
override fun source(): BufferedSource = wrappedSource override fun source(): BufferedSource = wrappedSource
// To avoid double buffering, We have to make ForwardingBufferedSource. // To avoid double buffering, We have to make ForwardingBufferedSource.
@Suppress("TooManyFunctions")
internal open class ForwardingBufferedSource( internal open class ForwardingBufferedSource(
private val originalSource: BufferedSource, private val originalSource: BufferedSource,
) : BufferedSource { ) : BufferedSource {

View File

@ -1,5 +1,7 @@
package jp.juggler.util package jp.juggler.util
import java.util.LinkedHashMap
// same as x?.let{ dst.add(it) } // same as x?.let{ dst.add(it) }
fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this) fun <T> T.addTo(dst: ArrayList<T>) = dst.add(this)
@ -11,3 +13,6 @@ fun <E : Map<*, *>> E?.notEmpty(): E? =
fun ByteArray?.notEmpty(): ByteArray? = fun ByteArray?.notEmpty(): ByteArray? =
if (this?.isNotEmpty() == true) this else null if (this?.isNotEmpty() == true) this else null
fun <K, V : Any?> Iterable<Pair<K, V>>.toMutableMap() =
LinkedHashMap<K, V>().also { map -> forEach { map[it.first] = it.second } }