(WIP)(Mastodon 3.1.0)ホームカラムに告知を表示する

This commit is contained in:
tateisu 2020-01-27 14:45:16 +09:00
parent 7f4b93dc38
commit 99e2efb9bb
22 changed files with 1007 additions and 71 deletions

View File

@ -487,6 +487,18 @@ class Column(
internal var language_filter : JsonObject? = null internal var language_filter : JsonObject? = null
// 告知のリスト
internal var announcements : MutableList<TootAnnouncement>? = null
// 表示中の告知
internal var announcementId : EntityId? = null
// 告知を閉じた時刻, 0なら閉じていない
internal var announcementHideTime = 0L
// 告知データを更新したタイミング
internal var announcementUpdated = 0L
// プロフカラムでのアカウント情報 // プロフカラムでのアカウント情報
@Volatile @Volatile
internal var who_account : TootAccountRef? = null internal var who_account : TootAccountRef? = null
@ -2572,6 +2584,65 @@ class Column(
} }
} }
} }
override fun onAnnouncementUpdate(item : TootAnnouncement) {
val list = announcements
if(list == null) {
announcements = mutableListOf(item)
}else{
val index = list.indexOfFirst{ it.id == item.id}
if( index != -1 ){
list[index] = TootAnnouncement.merge(list[index],item)
}else{
list.add(0, item)
}
announcements?.sortWith(TootAnnouncement.comparator)
}
announcementUpdated = SystemClock.elapsedRealtime()
fireShowColumnHeader()
}
override fun onAnnouncementDelete(id : EntityId) {
val it = announcements?.iterator() ?: return
while(it.hasNext()){
val item = it.next()
if( item.id == id){
it.remove()
announcementUpdated = SystemClock.elapsedRealtime()
fireShowColumnHeader()
return
}
}
}
override fun onAnnouncementReaction(reaction : TootAnnouncement.Reaction) {
// find announcement
val announcement_id = reaction.announcement_id ?: return
val announcement = announcements?.find { it.id == announcement_id } ?: return
// find reaction
val index = announcement.reactions?.indexOfFirst { it.name == reaction.name }
when {
reaction.count <= 0L -> {
if(index != null && index != - 1) announcement.reactions?.removeAt(index)
}
index == null -> {
announcement.reactions = ArrayList<TootAnnouncement.Reaction>().apply {
add(reaction)
}
}
index == - 1 -> announcement.reactions?.add(reaction)
else -> announcement.reactions?.get(index)?.let{ old ->
old.count = reaction.count
// ストリーミングイベントにはmeが含まれないので、oldにあるmeは変更されない
}
}
announcementUpdated = SystemClock.elapsedRealtime()
fireShowColumnHeader()
}
} }
internal fun resumeStreaming(bPutGap : Boolean) { internal fun resumeStreaming(bPutGap : Boolean) {

View File

@ -43,9 +43,16 @@ abstract class ColumnTask(
val highlight_trie : WordTrieTree? val highlight_trie : WordTrieTree?
get() = column.highlight_trie get() = column.highlight_trie
val isPseudo :Boolean
get() = access_info.isPseudo
val isMastodon : Boolean
get() = access_info.isMastodon
val isMisskey : Boolean val isMisskey : Boolean
get() = access_info.isMisskey get() = access_info.isMisskey
val misskeyVersion : Int val misskeyVersion : Int
get() = access_info.misskeyVersion get() = access_info.misskeyVersion

View File

@ -1039,5 +1039,31 @@ class ColumnTask_Loading(
// fallback to old api // fallback to old api
return getStatusList(client, Column.PATH_DIRECT_MESSAGES) return getStatusList(client, Column.PATH_DIRECT_MESSAGES)
} }
internal fun getAnnouncements(client : TootApiClient) : TootApiResult? {
if( isMastodon && !isPseudo ){
column.announcements = null
column.announcementUpdated =1L
column.announcementId = null
client.publishApiProgress("loading announcements")
val (instance, _) = TootInstance.get(client)
if( instance?.versionGE(TootInstance.VERSION_3_1_0_rc1) == true){
val result = client.request("/api/v1/announcements")
?: return null // cancelled.
val code = result.response?.code ?: 0
if(code !in 400 until 500) {
val list = parseList(::TootAnnouncement,parser,result.jsonArray)
if(list.isNotEmpty()){
column.announcements = list
column.announcementUpdated = SystemClock.elapsedRealtime()
client.publishApiProgress("announcements loaded")
}
// other errors such as network or server fails will stop column loading.
return result
}
// just skip load announcements for 4xx error if server does not support announcements.
}
}
return TootApiResult()
}
} }

View File

@ -398,7 +398,10 @@ enum class ColumnType(
iconId = { R.drawable.ic_home }, iconId = { R.drawable.ic_home },
name1 = { it.getString(R.string.home) }, name1 = { it.getString(R.string.home) },
loading = { client -> getStatusList(client, column.makeHomeTlUrl()) }, loading = { client ->
getAnnouncements(client)
getStatusList(client, column.makeHomeTlUrl())
},
refresh = { client -> getStatusList(client, column.makeHomeTlUrl()) }, refresh = { client -> getStatusList(client, column.makeHomeTlUrl()) },
gap = { client -> getStatusList(client, column.makeHomeTlUrl()) }, gap = { client -> getStatusList(client, column.makeHomeTlUrl()) },
bAllowPseudo = false bAllowPseudo = false

View File

@ -6,7 +6,9 @@ import android.content.res.ColorStateList
import android.graphics.Bitmap import android.graphics.Bitmap
import android.graphics.Color import android.graphics.Color
import android.os.AsyncTask import android.os.AsyncTask
import android.os.SystemClock
import android.text.InputType import android.text.InputType
import android.text.Spannable
import android.text.SpannableStringBuilder import android.text.SpannableStringBuilder
import android.text.TextUtils import android.text.TextUtils
import android.view.* import android.view.*
@ -17,13 +19,26 @@ import android.widget.*
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.google.android.flexbox.FlexWrap
import com.google.android.flexbox.FlexboxLayout
import com.google.android.flexbox.JustifyContent
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayout import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayout
import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection import com.omadahealth.github.swipyrefreshlayout.library.SwipyRefreshLayoutDirection
import jp.juggler.subwaytooter.action.Action_List import jp.juggler.subwaytooter.action.Action_List
import jp.juggler.subwaytooter.action.Action_Notification import jp.juggler.subwaytooter.action.Action_Notification
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootTask
import jp.juggler.subwaytooter.api.TootTaskRunner
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
import jp.juggler.subwaytooter.api.entity.TootStatus
import jp.juggler.subwaytooter.dialog.EmojiPicker
import jp.juggler.subwaytooter.span.NetworkEmojiSpan
import jp.juggler.subwaytooter.table.AcctColor import jp.juggler.subwaytooter.table.AcctColor
import jp.juggler.subwaytooter.util.* import jp.juggler.subwaytooter.util.*
import jp.juggler.subwaytooter.view.ListDivider import jp.juggler.subwaytooter.view.ListDivider
import jp.juggler.subwaytooter.view.MyLinkMovementMethod
import jp.juggler.subwaytooter.view.MyTextView
import jp.juggler.util.* import jp.juggler.util.*
import org.jetbrains.anko.* import org.jetbrains.anko.*
import java.io.Closeable import java.io.Closeable
@ -137,6 +152,22 @@ class ColumnViewHolder(
private lateinit var etHashtagExtraAll : EditText private lateinit var etHashtagExtraAll : EditText
private lateinit var etHashtagExtraNone : EditText private lateinit var etHashtagExtraNone : EditText
private lateinit var llAnnouncementsBox : View
private lateinit var tvAnnouncementsIndex : TextView
private lateinit var btnAnnouncementsPrev : ImageButton
private lateinit var btnAnnouncementsNext : ImageButton
private lateinit var btnAnnouncementsShowHide : ImageButton
private lateinit var llAnnouncements : View
private lateinit var tvAnnouncementPeriod : TextView
private lateinit var tvAnnouncementContent : MyTextView
private lateinit var llAnnouncementExtra : LinearLayout
private val announcementContentInvalidator : NetworkEmojiInvalidator
private var lastAnnouncementShown = 0L
private val extra_invalidator_list = ArrayList<NetworkEmojiInvalidator>()
private val isPageDestroyed : Boolean private val isPageDestroyed : Boolean
get() = column == null || activity.isFinishing get() = column == null || activity.isFinishing
@ -291,8 +322,6 @@ class ColumnViewHolder(
handled handled
} }
btnQuickFilterAll.setOnClickListener(this) btnQuickFilterAll.setOnClickListener(this)
btnQuickFilterMention.setOnClickListener(this) btnQuickFilterMention.setOnClickListener(this)
btnQuickFilterFavourite.setOnClickListener(this) btnQuickFilterFavourite.setOnClickListener(this)
@ -301,7 +330,6 @@ class ColumnViewHolder(
btnQuickFilterReaction.setOnClickListener(this) btnQuickFilterReaction.setOnClickListener(this)
btnQuickFilterVote.setOnClickListener(this) btnQuickFilterVote.setOnClickListener(this)
llColumnHeader.setOnClickListener(this) llColumnHeader.setOnClickListener(this)
btnColumnSetting.setOnClickListener(this) btnColumnSetting.setOnClickListener(this)
btnColumnReload.setOnClickListener(this) btnColumnReload.setOnClickListener(this)
@ -317,6 +345,10 @@ class ColumnViewHolder(
llRefreshError.setOnClickListener(this) llRefreshError.setOnClickListener(this)
btnAnnouncementsShowHide.setOnClickListener(this)
btnAnnouncementsPrev.setOnClickListener(this)
btnAnnouncementsNext.setOnClickListener(this)
cbDontCloseColumn.setOnCheckedChangeListener(this) cbDontCloseColumn.setOnCheckedChangeListener(this)
cbWithAttachment.setOnCheckedChangeListener(this) cbWithAttachment.setOnCheckedChangeListener(this)
@ -426,6 +458,10 @@ class ColumnViewHolder(
activity.handler.removeCallbacks(proc_start_filter) activity.handler.removeCallbacks(proc_start_filter)
activity.handler.postDelayed(proc_start_filter, 666L) activity.handler.postDelayed(proc_start_filter, 666L)
}) })
announcementContentInvalidator =
NetworkEmojiInvalidator(activity.handler, tvAnnouncementContent)
tvAnnouncementContent.movementMethod = MyLinkMovementMethod
} }
private val proc_start_filter : Runnable = Runnable { private val proc_start_filter : Runnable = Runnable {
@ -611,7 +647,7 @@ class ColumnViewHolder(
btnDeleteNotification.vg(column.isNotificationColumn) btnDeleteNotification.vg(column.isNotificationColumn)
llSearch.vg(column.isSearchColumn)?.let{ llSearch.vg(column.isSearchColumn)?.let {
btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref)) btnSearchClear.vg(Pref.bpShowSearchClear(activity.pref))
} }
@ -652,6 +688,8 @@ class ColumnViewHolder(
column.addColumnViewHolder(this) column.addColumnViewHolder(this)
lastAnnouncementShown = - 1L
showColumnColor() showColumnColor()
showContent(reason = "onPageCreate", reset = true) showContent(reason = "onPageCreate", reset = true)
@ -1060,8 +1098,30 @@ class ColumnViewHolder(
btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION) btnQuickFilterReaction -> clickQuickFilter(Column.QUICK_FILTER_REACTION)
btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE) btnQuickFilterVote -> clickQuickFilter(Column.QUICK_FILTER_VOTE)
btnAnnouncementsShowHide -> {
if(llAnnouncements.visibility == View.VISIBLE) {
column.announcementHideTime = System.currentTimeMillis()
} else {
column.announcementHideTime = 0L
}
activity.app_state.saveColumnList()
showAnnouncements()
}
btnAnnouncementsPrev -> {
column.announcementId =
TootAnnouncement.move(column.announcements, column.announcementId, - 1)
activity.app_state.saveColumnList()
showAnnouncements()
}
btnAnnouncementsNext -> {
column.announcementId =
TootAnnouncement.move(column.announcements, column.announcementId, + 1)
activity.app_state.saveColumnList()
showAnnouncements()
}
} }
} }
override fun onLongClick(v : View) : Boolean { override fun onLongClick(v : View) : Boolean {
@ -1130,12 +1190,14 @@ class ColumnViewHolder(
showColumnCloseButton() showColumnCloseButton()
showAnnouncements(force = false)
} }
// カラムヘッダなど、負荷が低い部分の表示更新 // カラムヘッダなど、負荷が低い部分の表示更新
fun showColumnHeader() { fun showColumnHeader() {
activity.handler.removeCallbacks(procShowColumnHeader) activity.handler.removeCallbacks(procShowColumnHeader)
activity.handler.postDelayed(procShowColumnHeader, 50L) activity.handler.postDelayed(procShowColumnHeader, 50L)
} }
internal fun showContent( internal fun showContent(
@ -1198,6 +1260,7 @@ class ColumnViewHolder(
showRefreshError() showRefreshError()
} }
proc_restoreScrollPosition.run() proc_restoreScrollPosition.run()
} }
private var bRefreshErrorWillShown = false private var bRefreshErrorWillShown = false
@ -1887,6 +1950,121 @@ class ColumnViewHolder(
} // end of column setting scroll view } // end of column setting scroll view
llAnnouncementsBox = verticalLayout {
lparams(matchParent, wrapContent) {
startMargin = dip(6)
endMargin = dip(6)
topMargin = dip(2)
bottomMargin = dip(2)
}
background = createRoundDrawable(
dip(6).toFloat(),
getAttributeColor(context, R.attr.colorThumbnailBackground)
)
var pad_tb = dip(2)
setPadding(0, pad_tb, 0, pad_tb)
linearLayout {
lparams(matchParent, wrapContent) {
startMargin = dip(6)
endMargin = dip(6)
}
gravity = Gravity.CENTER_VERTICAL
textView {
gravity = Gravity.END
text = context.getString(R.string.announcements)
}.lparams(0, wrapContent) {
weight = 1f
}
btnAnnouncementsPrev = imageButton {
background = ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent
)
contentDescription = context.getString(R.string.previous)
imageResource = R.drawable.ic_arrow_start
}.lparams(dip(32), dip(32)) {
gravity = Gravity.END
marginStart = dip(4)
}
tvAnnouncementsIndex = textView {
}.lparams(wrapContent, wrapContent) {
marginStart = dip(4)
}
btnAnnouncementsNext = imageButton {
background = ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent
)
contentDescription = context.getString(R.string.next)
imageResource = R.drawable.ic_arrow_end
}.lparams(dip(32), dip(32)) {
gravity = Gravity.END
marginStart = dip(4)
}
btnAnnouncementsShowHide = imageButton {
background = ContextCompat.getDrawable(
context,
R.drawable.btn_bg_transparent
)
contentDescription = context.getString(R.string.hide)
imageResource = R.drawable.ic_close
}.lparams(dip(32), dip(32)) {
gravity = Gravity.END
marginStart = dip(4)
}
}
llAnnouncements = maxHeightScrollView {
lparams(matchParent, wrapContent) {
topMargin = dip(1)
}
val pad_lr = dip(6)
pad_tb = dip(2)
setPadding(pad_lr, pad_tb, pad_lr, pad_tb)
scrollBarStyle = View.SCROLLBARS_OUTSIDE_OVERLAY
isScrollbarFadingEnabled = false
maxHeight = dip(240)
verticalLayout {
lparams(matchParent, wrapContent)
// 期間があれば表示する
tvAnnouncementPeriod = textView {
gravity = Gravity.END
}.lparams(matchParent, wrapContent) {
bottomMargin = dip(3)
}
tvAnnouncementContent = myTextView {
setLineSpacing(lineSpacingExtra, 1.1f)
// tools:text="Contents\nContents"
}.lparams(matchParent, wrapContent) {
topMargin = dip(3)
}
llAnnouncementExtra = verticalLayout {
lparams(matchParent, wrapContent) {
topMargin = dip(3)
}
}
}
}
}
llSearch = verticalLayout { llSearch = verticalLayout {
lparams(matchParent, wrapContent) lparams(matchParent, wrapContent)
backgroundColor = getAttributeColor(context, R.attr.colorSearchFormBackground) backgroundColor = getAttributeColor(context, R.attr.colorSearchFormBackground)
@ -2112,4 +2290,367 @@ class ColumnViewHolder(
b.report() b.report()
rv rv
} }
private fun showAnnouncements(force : Boolean = true) {
val column = column ?: return
if(! force && lastAnnouncementShown >= column.announcementUpdated) {
return
}
lastAnnouncementShown = SystemClock.elapsedRealtime()
fun clearExtras() {
for(invalidator in extra_invalidator_list) {
invalidator.register(null)
}
extra_invalidator_list.clear()
}
llAnnouncementExtra.removeAllViews()
clearExtras()
val listShown = TootAnnouncement.filterShown(column.announcements)
if(llAnnouncementsBox.vg(listShown?.isNotEmpty() == true) == null) {
return
}
val content_color = column.getContentColor()
val item = listShown !!.find { it.id == column.announcementId }
?: listShown[0]
val itemIndex = listShown.indexOf(item)
val enablePaging = listShown.size > 1
val expand = column.announcementHideTime <= 0L
btnAnnouncementsPrev.vg(expand)?.run {
isEnabled = enablePaging
alpha = if(enablePaging) 1f else 0.3f
}
btnAnnouncementsNext.vg(expand)?.run {
isEnabled = enablePaging
alpha = if(enablePaging) 1f else 0.3f
}
tvAnnouncementsIndex.vg(expand)?.text =
activity.getString(R.string.announcements_index, itemIndex + 1, listShown.size)
llAnnouncements.vg(expand)
if(! expand) {
val newer = listShown.find { it.updated_at > column.announcementHideTime }
if(newer != null) {
column.announcementId = newer.id
setIconDrawableId(
activity,
btnAnnouncementsShowHide,
R.drawable.ic_error,
color = getAttributeColor(activity, R.attr.colorRegexFilterError),
alphaMultiplier = Styler.boost_alpha
)
} else {
setIconDrawableId(
activity,
btnAnnouncementsShowHide,
R.drawable.ic_arrow_drop_down,
color = content_color,
alphaMultiplier = Styler.boost_alpha
)
}
return
}
setIconDrawableId(
activity,
btnAnnouncementsShowHide,
R.drawable.ic_arrow_drop_up,
color = content_color,
alphaMultiplier = Styler.boost_alpha
)
var periods : StringBuilder? = null
fun String.appendPeriod() {
val sb = periods
if(sb == null) {
periods = StringBuilder(this)
} else {
sb.append("\n")
sb.append(this)
}
}
val (strStart, strEnd) = TootStatus.formatTimeRange(
item.starts_at,
item.ends_at,
item.all_day
)
when {
// no periods.
strStart == "" && strEnd == "" -> {
}
// single date
strStart == strEnd -> {
activity.getString(R.string.announcements_period1, strStart)
.appendPeriod()
}
else -> {
activity.getString(R.string.announcements_period2, strStart, strEnd)
.appendPeriod()
}
}
if(item.updated_at > item.published_at) {
val strUpdateAt = TootStatus.formatTime(activity, item.updated_at, false)
activity.getString(R.string.edited_at, strUpdateAt).appendPeriod()
}
val sb = periods
tvAnnouncementPeriod.vg(sb != null)?.text = sb
tvAnnouncementContent.text = item.decoded_content
announcementContentInvalidator.register(item.decoded_content)
// リアクションの表示
val density = activity.density
val buttonHeight = ActMain.boostButtonSize
val marginBetween = (buttonHeight.toFloat() * 0.2f + 0.5f).toInt()
val paddingH = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
val paddingV = (buttonHeight.toFloat() * 0.1f + 0.5f).toInt()
val box = FlexboxLayout(activity).apply {
flexWrap = FlexWrap.WRAP
justifyContent = JustifyContent.FLEX_START
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
topMargin = (0.5f + density * 3f).toInt()
}
}
// +ボタン
run {
val b = ImageButton(activity)
val blp = FlexboxLayout.LayoutParams(
buttonHeight,
buttonHeight
)
blp.endMargin = marginBetween
b.layoutParams = blp
b.background = ContextCompat.getDrawable(
activity,
R.drawable.btn_bg_transparent
)
b.contentDescription = activity.getString(R.string.reaction_add)
b.scaleType = ImageView.ScaleType.FIT_CENTER
b.padding = paddingV
b.setOnClickListener {
addReaction(item, null)
}
setIconDrawableId(
activity,
b,
R.drawable.ic_add,
color = content_color,
alphaMultiplier = Styler.boost_alpha
)
box.addView(b)
}
val reactions = item.reactions?.filter { it.count > 0L }?.notEmpty()
if(reactions != null) {
var lastButton : View? = null
val options = DecodeOptions(
activity,
column.access_info,
decodeEmoji = true,
enlargeEmoji = 1.5f
)
val actMain = activity
val emojiAnimation = Pref.bpDisableEmojiAnimation(actMain.pref)
for(reaction in reactions) {
val url = if(emojiAnimation) {
reaction.url.notEmpty() ?: reaction.static_url.notEmpty()
} else {
reaction.static_url.notEmpty() ?: reaction.url.notEmpty()
}
val b = Button(activity).apply {
layoutParams = FlexboxLayout.LayoutParams(
FlexboxLayout.LayoutParams.WRAP_CONTENT,
buttonHeight
).apply {
endMargin = marginBetween
}
minWidthCompat = buttonHeight
allCaps = false
tag = reaction
background = ContextCompat.getDrawable(
actMain,
if(reaction.me == true) {
R.drawable.bg_button_cw
} else {
R.drawable.btn_bg_transparent
}
)
setTextColor(content_color)
setPadding(paddingH, paddingV, paddingH, paddingV)
text = if(url == null) {
EmojiDecoder.decodeEmoji(options, "${reaction.name} ${reaction.count}")
} else {
SpannableStringBuilder("${reaction.name} ${reaction.count}").also { sb ->
sb.setSpan(
NetworkEmojiSpan(url, scale = 1.5f),
0,
reaction.name.length,
Spannable.SPAN_EXCLUSIVE_EXCLUSIVE
)
val invalidator =
NetworkEmojiInvalidator(actMain.handler, this)
invalidator.register(sb)
extra_invalidator_list.add(invalidator)
}
}
setOnClickListener {
if(reaction.me == true) {
removeReaction(item, reaction.name)
} else {
addReaction(item, TootAnnouncement.Reaction(jsonObject {
put("name", reaction.name)
put("count", 1)
put("me", true)
putNotNull("url", reaction.url)
putNotNull("static_url", reaction.static_url)
}))
}
}
}
box.addView(b)
lastButton = b
}
lastButton
?.layoutParams
?.cast<ViewGroup.MarginLayoutParams>()
?.endMargin = 0
}
llAnnouncementExtra.addView(box)
}
private fun addReaction(item : TootAnnouncement, sample : TootAnnouncement.Reaction?) {
val column = column ?: return
val host = column.access_info.host
val isMisskey = column.isMisskey
if(sample == null) {
EmojiPicker(activity, host, isMisskey) { name, _, _ ,unicode->
log.d("addReaction: $name")
addReaction(item, TootAnnouncement.Reaction(jsonObject {
put("name", unicode ?: name )
put("count", 1)
put("me", true)
// 以下はカスタム絵文字のみ
if(unicode == null){
val map = App1.custom_emoji_lister.getMap(host, isMisskey)
if(map == null) {
showToast(activity, false, "emoji map is null")
return@EmojiPicker
}
val ce = map[name]
if(ce == null) {
showToast(activity, false, "emoji '$name' not found.")
return@EmojiPicker
}
putNotNull("url", ce.url)
putNotNull("static_url", ce.static_url)
}
}))
}.show()
return
}
TootTaskRunner(activity).run(column.access_info, object : TootTask {
override fun background(client : TootApiClient) : TootApiResult? {
return client.request(
"/api/v1/announcements/${item.id}/reactions/${sample.name.encodePercent()}",
JsonObject().toPutRequestBuilder()
)
// 200 {}
}
override fun handleResult(result : TootApiResult?) {
result ?: return
if(result.jsonObject == null) {
showToast(activity, true, result.error)
} else {
val list = item.reactions
if(list == null) {
item.reactions = mutableListOf(sample)
} else {
val reaction = list.find { it.name == sample.name }
if(reaction == null) {
list.add(sample)
} else {
reaction.me = true
++reaction.count
}
}
column.announcementUpdated = SystemClock.elapsedRealtime()
showAnnouncements()
}
}
})
}
private fun removeReaction(item : TootAnnouncement, name : String) {
val column = column ?: return
TootTaskRunner(activity).run(column.access_info, object : TootTask {
override fun background(client : TootApiClient) : TootApiResult? {
return client.request(
"/api/v1/announcements/${item.id}/reactions/${name.encodePercent()}",
JsonObject().toDeleteRequestBuilder()
)
// 200 {}
}
override fun handleResult(result : TootApiResult?) {
result ?: return
if(result.jsonObject == null) {
showToast(activity, true, result.error)
} else {
val it = item.reactions?.iterator() ?: return
while(it.hasNext()) {
val reaction = it.next()
if(reaction.name == name) {
reaction.me = false
if(-- reaction.count <= 0) it.remove()
break
}
}
column.announcementUpdated = SystemClock.elapsedRealtime()
showAnnouncements()
}
}
})
}
} }

View File

@ -3365,7 +3365,7 @@ internal class ItemViewHolder(
context, context,
R.drawable.btn_bg_transparent R.drawable.btn_bg_transparent
) )
contentDescription = "@string/hide" contentDescription = context.getString(R.string.hide)
imageResource = R.drawable.ic_close imageResource = R.drawable.ic_close
}.lparams(dip(32), dip(32)) { }.lparams(dip(32), dip(32)) {
gravity = Gravity.END gravity = Gravity.END

View File

@ -5,6 +5,7 @@ import android.content.SharedPreferences
import android.os.Handler import android.os.Handler
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.entity.TootAnnouncement.Reaction
import jp.juggler.subwaytooter.table.SavedAccount import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.util.* import jp.juggler.util.*
import okhttp3.Response import okhttp3.Response
@ -24,11 +25,14 @@ internal class StreamReader(
) { ) {
internal interface StreamCallback { internal interface StreamCallback {
fun channelId() : String?
fun onTimelineItem(item : TimelineItem) fun onTimelineItem(item : TimelineItem)
fun onListeningStateChanged(bListen : Boolean) fun onListeningStateChanged(bListen : Boolean)
fun onNoteUpdated(ev : MisskeyNoteUpdate) fun onNoteUpdated(ev : MisskeyNoteUpdate)
fun onAnnouncementUpdate( item: TootAnnouncement)
fun channelId() : String? fun onAnnouncementDelete( id: EntityId)
fun onAnnouncementReaction(reaction : Reaction)
} }
companion object { companion object {
@ -131,14 +135,12 @@ internal class StreamReader(
} }
} }
private fun fireTimelineItem(item : TimelineItem?, channelId : String? = null) { private inline fun eachCallback(block:(callback:StreamCallback)->Unit){
item ?: return
synchronized(this) { synchronized(this) {
if(bDisposed.get()) return@synchronized if(bDisposed.get()) return@synchronized
for(callback in callback_list) { for(callback in callback_list) {
try { try {
if(channelId != null && channelId != callback.channelId()) continue block(callback)
callback.onTimelineItem(item)
} catch(ex : Throwable) { } catch(ex : Throwable) {
log.trace(ex) log.trace(ex)
} }
@ -146,12 +148,21 @@ internal class StreamReader(
} }
} }
private fun fireTimelineItem(item : TimelineItem?, channelId : String? = null) {
item ?: return
eachCallback{ callback->
if(channelId != null && channelId != callback.channelId()) return@eachCallback
callback.onTimelineItem(item)
}
}
private fun fireDeleteId(id : EntityId) { private fun fireDeleteId(id : EntityId) {
val tl_host = access_info.host val tl_host = access_info.host
runOnMainLooper { runOnMainLooper {
synchronized(this) { synchronized(this) {
if(bDisposed.get()) return@runOnMainLooper if(bDisposed.get()) return@synchronized
if(Pref.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return@runOnMainLooper if(Pref.bpDontRemoveDeletedToot(App1.getAppState(context).pref)) return@synchronized
for(column in App1.getAppState(context).column_list) { for(column in App1.getAppState(context).column_list) {
try { try {
column.onStatusRemoved(tl_host, id) column.onStatusRemoved(tl_host, id)
@ -165,16 +176,9 @@ internal class StreamReader(
private fun fireNoteUpdated(ev : MisskeyNoteUpdate, channelId : String? = null) { private fun fireNoteUpdated(ev : MisskeyNoteUpdate, channelId : String? = null) {
runOnMainLooper { runOnMainLooper {
synchronized(this) { eachCallback { callback->
if(bDisposed.get()) return@runOnMainLooper if(channelId != null && channelId != callback.channelId()) return@eachCallback
for(callback in callback_list) { callback.onNoteUpdated(ev)
try {
if(channelId != null && channelId != callback.channelId()) continue
callback.onNoteUpdated(ev)
} catch(ex : Throwable) {
log.trace(ex)
}
}
} }
} }
} }
@ -252,6 +256,61 @@ internal class StreamReader(
} }
private fun handleMastodonMessage(obj:JsonObject,text:String){
when(val event = obj.string("event")) {
null,"" -> log.d("onMessage: missing event parameter")
"filters_changed" ->
Column.onFiltersChanged(context, access_info)
else -> {
val payload = TootPayload.parsePayload(parser, event, obj, text)
when(event) {
"delete" -> when(payload) {
is Long -> fireDeleteId(EntityId(payload.toString()))
is String -> fireDeleteId(EntityId(payload.toString()))
else -> log.d("unsupported payload type. $payload")
}
// {"event":"announcement","payload":"{\"id\":\"3\",\"content\":\"<p>追加</p>\",\"starts_at\":null,\"ends_at\":null,\"all_day\":false,\"mentions\":[],\"tags\":[],\"emojis\":[],\"reactions\":[]}"}
"announcement"->{
if( payload is TootAnnouncement) {
runOnMainLooper {
eachCallback { it.onAnnouncementUpdate(payload) }
}
}
}
// {"event":"announcement.delete","payload":"2"}
"announcement.delete"->{
val id = EntityId.mayNull(payload?.toString())
if( id != null){
runOnMainLooper {
eachCallback { it.onAnnouncementDelete(id) }
}
}
}
// {"event":"announcement.reaction","payload":"{\"name\":\"hourglass_gif\",\"count\":1,\"url\":\"https://m2j.zzz.ac/...\",\"static_url\":\"https://m2j.zzz.ac/...\",\"announcement_id\":\"9\"}"}
"announcement.reaction"->{
if( payload is Reaction) {
runOnMainLooper {
eachCallback { it.onAnnouncementReaction(payload) }
}
}
}
else -> when(payload) {
is TimelineItem -> fireTimelineItem(payload)
else -> log.d("unsupported payload type. $payload")
}
}
}
}
}
/** /**
* Invoked when a text (type `0x1`) message has been received. * Invoked when a text (type `0x1`) message has been received.
*/ */
@ -263,34 +322,8 @@ internal class StreamReader(
if(access_info.isMisskey) { if(access_info.isMisskey) {
handleMisskeyMessage(obj) handleMisskeyMessage(obj)
} else { } else {
handleMastodonMessage(obj,text)
val event = obj.string("event")
if(event == null || event.isEmpty()) {
log.d("onMessage: missing event parameter")
return
}
if(event == "filters_changed") {
Column.onFiltersChanged(context, access_info)
return
}
val payload = TootPayload.parsePayload(parser, event, obj, text)
when(event) {
"delete" -> when(payload) {
is Long -> fireDeleteId(EntityId(payload.toString()))
is String -> fireDeleteId(EntityId(payload.toString()))
else -> log.d("unsupported payload type. $payload")
}
else -> when(payload) {
is TimelineItem -> fireTimelineItem(payload)
else -> log.d("unsupported payload type. $payload")
}
}
} }
} catch(ex : Throwable) { } catch(ex : Throwable) {

View File

@ -0,0 +1,162 @@
package jp.juggler.subwaytooter.api.entity
import android.text.Spannable
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.util.DecodeOptions
import jp.juggler.util.JsonObject
import jp.juggler.util.LogCategory
import jp.juggler.util.notEmpty
import jp.juggler.util.notZero
import java.util.HashMap
import kotlin.Comparator
import kotlin.Int
import kotlin.String
class TootAnnouncement(parser : TootParser, src : JsonObject) {
class Reaction(val src : JsonObject) {
val name = src.string("name") ?: "?"
var count = src.long("count") ?: 0
var me = src.boolean("me") // ストリーミングイベントではmeは定義されない
// 以下はカスタム絵文字のみ
val url = src.string("url")
val static_url = src.string("static_url")
// ストリーミングイベントでは告知IDが含まれる
val announcement_id = EntityId.mayNull(src.string("announcement_id"))
}
// {"id":"1",
// "content":"\u003cp\u003e日本語\u003cbr /\u003eURL \u003ca href=\"https://www.youtube.com/watch?v=2n1fM2ItdL8\" rel=\"nofollow noopener noreferrer\" target=\"_blank\"\u003e\u003cspan class=\"invisible\"\u003ehttps://www.\u003c/span\u003e\u003cspan class=\"ellipsis\"\u003eyoutube.com/watch?v=2n1fM2ItdL\u003c/span\u003e\u003cspan class=\"invisible\"\u003e8\u003c/span\u003e\u003c/a\u003e\u003cbr /\u003eカスタム絵文字 :ct013: \u003cbr /\u003e普通の絵文字 🤹 \u003c/p\u003e\u003cp\u003e改行2つ\u003c/p\u003e",
// "starts_at":"2020-01-23T00:00:00.000Z",
// "ends_at":"2020-01-28T23:59:00.000Z",
// "all_day":true,
// "mentions":[],
// "tags":[],
// "emojis":[{"shortcode":"ct013","url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/original/ct013.png","static_url":"https://m2j.zzz.ac/custom_emojis/images/000/004/116/static/ct013.png","visible_in_picker":true}],
// "reactions":[]}]
val id = EntityId.mayDefault(src.string("id"))
val starts_at = TootStatus.parseTime(src.string("starts_at"))
val ends_at = TootStatus.parseTime(src.string("ends_at"))
val all_day = src.boolean("all_day") ?: false
val published_at = TootStatus.parseTime(src.string("published_at"))
val updated_at = TootStatus.parseTime(src.string("updated_at"))
private val custom_emojis : HashMap<String, CustomEmoji>?
// Body of the status; this will contain HTML (remote HTML already sanitized)
val content : String
val decoded_content : Spannable
//An array of Tags
val tags : ArrayList<TootTag>?
// An array of Mentions
val mentions : ArrayList<TootMention>?
var reactions : MutableList<Reaction>? = null
init {
// 絵文字マップはすぐ後で使うので、最初の方で読んでおく
this.custom_emojis =
parseMapOrNull(CustomEmoji.decode, src.jsonArray("emojis"), log)
this.tags = parseListOrNull(::TootTag, src.jsonArray("tags"))
this.mentions = parseListOrNull(::TootMention, src.jsonArray("mentions"), log)
val options = DecodeOptions(
parser.context,
parser.linkHelper,
short = true,
decodeEmoji = true,
emojiMapCustom = custom_emojis,
// emojiMapProfile = profile_emojis,
// attachmentList = media_attachments,
highlightTrie = parser.highlightTrie,
mentions = mentions
)
this.content = src.string("content") ?: ""
this.decoded_content = options.decodeHTML(content)
this.reactions = parseListOrNull(::Reaction, src.jsonArray("reactions"))
}
companion object {
private val log = LogCategory("TootAnnouncement")
val comparator = Comparator<TootAnnouncement> { a, b ->
val at = a.starts_at.notZero() ?: a.published_at.notZero() ?: 0L
val bt = b.starts_at.notZero() ?: b.published_at.notZero() ?: 0L
if(at < bt) - 1 else if(at > bt) 1 else 0
}
// return null if list is empty
fun filterShown(src : List<TootAnnouncement>?) : List<TootAnnouncement>? {
val now = System.currentTimeMillis()
return src
?.filter {
when {
// 期間の大小が入れ替わってる場合はフィルタしない
it.starts_at > it.ends_at -> true
// まだ開始していない
it.starts_at > 0L && now < it.starts_at -> false
// 終了した後
it.ends_at > 0L && now > it.ends_at -> false
// フィルタしない
else -> true
}
}
?.notEmpty()
?.sortedWith(comparator)
}
// return previous/next item in announcement list.
fun move(src : List<TootAnnouncement>?, currentId : EntityId?, delta : Int) : EntityId? {
val listShown = filterShown(src)
?: return null
val size = listShown.size
if(size <= 0) return null
val idx = delta + when(val v = listShown.indexOfFirst { it.id == currentId }) {
- 1 -> 0
else -> v
}
return listShown[(idx + size) % size].id
}
// https://github.com/tootsuite/mastodon/blob/b9d74d407673a6dbdc87c3310618b22c85358c85/app/javascript/mastodon/reducers/announcements.js#L64
// reactionsのmeを残したまま他の項目を更新したい
fun merge(old : TootAnnouncement, dst : TootAnnouncement) : TootAnnouncement {
val oldReactions = old.reactions
val dstReactions = dst.reactions
if(dstReactions == null) {
dst.reactions = oldReactions
} else if(oldReactions != null) {
val reactions = mutableListOf<Reaction>()
reactions.addAll(oldReactions)
for(newItem in dstReactions) {
val oldItem = reactions.find { it.name == newItem.name }
if(oldItem == null) {
reactions.add(newItem)
} else {
oldItem.count = newItem.count
}
}
dst.reactions = reactions
}
return dst
}
}
}

View File

@ -180,6 +180,7 @@ class TootInstance(parser : TootParser, src : JsonObject) {
val VERSION_2_6_0 = VersionString("2.6.0") val VERSION_2_6_0 = VersionString("2.6.0")
val VERSION_2_7_0_rc1 = VersionString("2.7.0rc1") val VERSION_2_7_0_rc1 = VersionString("2.7.0rc1")
val VERSION_3_0_0_rc1 = VersionString("3.0.0rc1") val VERSION_3_0_0_rc1 = VersionString("3.0.0rc1")
val VERSION_3_1_0_rc1 = VersionString("3.1.0rc1")
val MISSKEY_VERSION_11 = VersionString("11.0") val MISSKEY_VERSION_11 = VersionString("11.0")

View File

@ -1,6 +1,7 @@
package jp.juggler.subwaytooter.api.entity package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.api.TootParser import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.api.entity.TootAnnouncement.Reaction
import jp.juggler.util.* import jp.juggler.util.*
import java.util.regex.Pattern import java.util.regex.Pattern
@ -61,10 +62,13 @@ object TootPayload {
"conversation" -> parseItem(::TootConversationSummary, parser, src) "conversation" -> parseItem(::TootConversationSummary, parser, src)
// ここを通るケースはまだ確認できていない "announcement" -> parseItem(::TootAnnouncement, parser, src)
"announcement.reaction" -> parseItem(::Reaction, src)
else -> { else -> {
log.e("unknown payload(2). message=%s", parent_text) log.e("unknown payload(2). message=%s", parent_text)
null // ここを通るケースはまだ確認できていない
} }
} }
} else if(payload[0] == '[') { } else if(payload[0] == '[') {

View File

@ -22,6 +22,7 @@ import java.util.regex.Pattern
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
import kotlin.collections.LinkedHashMap import kotlin.collections.LinkedHashMap
import kotlin.math.abs import kotlin.math.abs
import kotlin.math.min
class FilterTrees( class FilterTrees(
val treeIrreversible : WordTrieTree = WordTrieTree(), val treeIrreversible : WordTrieTree = WordTrieTree(),
@ -995,6 +996,9 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() {
@SuppressLint("SimpleDateFormat") @SuppressLint("SimpleDateFormat")
internal val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss") internal val date_format = SimpleDateFormat("yyyy-MM-dd HH:mm:ss")
@SuppressLint("SimpleDateFormat")
internal val date_format2 = SimpleDateFormat("yyyy-MM-dd")
fun formatTime(context : Context, t : Long, bAllowRelative : Boolean) : String { fun formatTime(context : Context, t : Long, bAllowRelative : Boolean) : String {
if(bAllowRelative && Pref.bpRelativeTimestamp(App1.pref)) { if(bAllowRelative && Pref.bpRelativeTimestamp(App1.pref)) {
@ -1050,7 +1054,53 @@ class TootStatus(parser : TootParser, src : JsonObject) : TimelineItem() {
} }
} }
return date_format.format(Date(t)) return formatDate(t,date_format,omitZeroSecond = false)
}
// 告知の開始/終了日付
private fun formatDate(
t : Long,
format:SimpleDateFormat ,
omitZeroSecond:Boolean
) : String {
var dateTarget = format.format(Date(t))
// 秒の部分を省略する
if( omitZeroSecond && dateTarget.endsWith(":00")){
dateTarget = dateTarget.substring(0,dateTarget.length -3)
}
// 年の部分が現在と同じなら省略する
val dateNow = format.format(Date(t))
val delm = dateNow.indexOf('-')
if(delm!=-1 && dateNow.substring(0,delm+1) == dateTarget.substring(0,delm+1)){
dateTarget = dateTarget.substring(delm+1)
}
return dateTarget
}
fun formatTimeRange(start : Long, end : Long, allDay : Boolean):Pair<String,String>{
val strStart = when {
start <= 0L -> ""
allDay-> formatDate(start,date_format2,omitZeroSecond = false)
else -> formatDate(start, date_format,omitZeroSecond = true)
}
val strEnd = when {
end <= 0L -> ""
allDay-> formatDate(end,date_format2,omitZeroSecond = false)
else -> formatDate(end, date_format,omitZeroSecond = true)
}
// 終了日は先頭と同じ部分を省略する
var skip = 0
for(i in 0 until min(strStart.length,strEnd.length)){
val c =strStart[i]
if(c != strEnd[i] ) break
if( c.isDigit() ) continue
skip= i+1
if( c == ' ') break // 時間以降は省略しない
}
return Pair( strStart,strEnd.substring(skip,strEnd.length))
} }
fun parseStringArray(src : JsonArray?) : ArrayList<String>? { fun parseStringArray(src : JsonArray?) : ArrayList<String>? {

View File

@ -30,7 +30,12 @@ class EmojiPicker(
private val activity : Activity, private val activity : Activity,
private val instance : String?, private val instance : String?,
@Suppress("CanBeParameter") private val isMisskey : Boolean, @Suppress("CanBeParameter") private val isMisskey : Boolean,
private val onEmojiPicked : (name : String, instance : String?, bInstanceHasCustomEmoji : Boolean) -> Unit private val onEmojiPicked : (
name : String,
instance : String?,
bInstanceHasCustomEmoji : Boolean,
unicode:String?
) -> Unit
// onEmojiPickedのinstance引数は通常の絵文字ならnull、カスタム絵文字なら非null、 // onEmojiPickedのinstance引数は通常の絵文字ならnull、カスタム絵文字なら非null、
) : View.OnClickListener, ViewPager.OnPageChangeListener { ) : View.OnClickListener, ViewPager.OnPageChangeListener {
@ -501,26 +506,28 @@ class EmojiPicker(
var name = item.name var name = item.name
if(item.instance != null && item.instance.isNotEmpty()) { if(item.instance != null && item.instance.isNotEmpty()) {
// カスタム絵文字 // カスタム絵文字
selected(name, item.instance) selected(name, item.instance,null)
} else { } else {
// 普通の絵文字 // 普通の絵文字
EmojiMap.sShortNameToEmojiInfo[name] ?: return var ei = EmojiMap.sShortNameToEmojiInfo[name] ?: return
if(page.hasSkinTone) { if(page.hasSkinTone) {
val sv = applySkinTone(name) val sv = applySkinTone(name)
if(EmojiMap.sShortNameToEmojiInfo[sv] != null) { val tmp = EmojiMap.sShortNameToEmojiInfo[sv]
if( tmp!=null){
ei = tmp
name = sv name = sv
} }
} }
selected(name, null) selected(name, null,ei.unified)
} }
} }
} }
} }
// name はスキントーン適用済みであること // name はスキントーン適用済みであること
internal fun selected(name : String, instance : String?) { internal fun selected(name : String, instance : String?,unicode:String?) {
dialog.dismissSafe() dialog.dismissSafe()
@ -564,7 +571,7 @@ class EmojiPicker(
} }
onEmojiPicked(name, instance, bInstanceHasCustomEmoji) onEmojiPicked(name, instance, bInstanceHasCustomEmoji,unicode)
} }
internal inner class EmojiPickerPagerAdapter : androidx.viewpager.widget.PagerAdapter() { internal inner class EmojiPickerPagerAdapter : androidx.viewpager.widget.PagerAdapter() {

View File

@ -35,7 +35,7 @@ var View.endPadding : Int
} }
// paddingStart,paddingEndにはsetterが提供されてない問題の対策 // paddingStart,paddingEndにはsetterが提供されてない問題の対策
fun View.setPaddingStartEnd(start : Int, end : Int) { fun View.setPaddingStartEnd(start : Int, end : Int =start) {
setPaddingRelative(start, paddingTop, end, paddingBottom) setPaddingRelative(start, paddingTop, end, paddingBottom)
} }

View File

@ -1014,7 +1014,7 @@ class PostHelper(
} }
private val open_picker_emoji : Runnable = Runnable { private val open_picker_emoji : Runnable = Runnable {
EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji -> EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji,_ ->
val et = this.et ?: return@EmojiPicker val et = this.et ?: return@EmojiPicker
val src = et.text ?: "" val src = et.text ?: ""
@ -1042,7 +1042,7 @@ class PostHelper(
} }
fun openEmojiPickerFromMore() { fun openEmojiPickerFromMore() {
EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji -> EmojiPicker(activity, instance, isMisskey) { name, instance, bInstanceHasCustomEmoji,_ ->
val et = this.et ?: return@EmojiPicker val et = this.et ?: return@EmojiPicker
val src = et.text ?: "" val src = et.text ?: ""

View File

@ -23,8 +23,8 @@ fun RequestBody.toPost() : Request.Builder =
fun RequestBody.toPut() :Request.Builder = fun RequestBody.toPut() :Request.Builder =
Request.Builder().put(this) Request.Builder().put(this)
// fun RequestBody.toDelete():Request.Builder = fun RequestBody.toDelete():Request.Builder =
// Request.Builder().delete(this) Request.Builder().delete(this)
fun RequestBody.toPatch() :Request.Builder = fun RequestBody.toPatch() :Request.Builder =
Request.Builder().patch(this) Request.Builder().patch(this)
@ -34,3 +34,4 @@ fun RequestBody.toRequest(methodArg : String) :Request.Builder =
fun JsonObject.toPostRequestBuilder() : Request.Builder = toRequestBody( ).toPost() fun JsonObject.toPostRequestBuilder() : Request.Builder = toRequestBody( ).toPost()
fun JsonObject.toPutRequestBuilder() : Request.Builder = toRequestBody( ).toPut() fun JsonObject.toPutRequestBuilder() : Request.Builder = toRequestBody( ).toPut()
fun JsonObject.toDeleteRequestBuilder() : Request.Builder = toRequestBody( ).toDelete()

View File

@ -3,6 +3,7 @@ package jp.juggler.util
import android.app.Activity import android.app.Activity
import android.content.Context import android.content.Context
import android.content.ContextWrapper import android.content.ContextWrapper
import android.graphics.drawable.GradientDrawable
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.view.inputmethod.InputMethodManager import android.view.inputmethod.InputMethodManager
@ -86,3 +87,11 @@ var CompoundButton.isCheckedNoAnime : Boolean
isChecked = value isChecked = value
jumpDrawablesToCurrentState() jumpDrawablesToCurrentState()
} }
fun createRoundDrawable(radius:Float,fillColor:Int?=null, strokeColor:Int?=null, strokeWidth:Int = 4) =
GradientDrawable().apply{
setCornerRadius(radius)
if(fillColor!=null) setColor(fillColor)
if( strokeColor!=null) setStroke(strokeWidth,strokeColor)
}

View File

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path

View File

@ -1,6 +1,7 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp" android:width="24dp"
android:height="24dp" android:height="24dp"
android:autoMirrored="true"
android:viewportWidth="24.0" android:viewportWidth="24.0"
android:viewportHeight="24.0"> android:viewportHeight="24.0">
<path <path

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M12,4.5C7,4.5 2.73,7.61 1,12c1.73,4.39 6,7.5 11,7.5s9.27,-3.11 11,-7.5c-1.73,-4.39 -6,-7.5 -11,-7.5zM12,17c-2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5 -2.24,5 -5,5zM12,9c-1.66,0 -3,1.34 -3,3s1.34,3 3,3 3,-1.34 3,-3 -1.34,-3 -3,-3z"/>
</vector>

View File

@ -83,7 +83,7 @@
android:contentDescription="@string/previous" android:contentDescription="@string/previous"
android:minWidth="48dp" android:minWidth="48dp"
android:src="@drawable/ic_left" android:src="@drawable/ic_arrow_start"
android:tint="?attr/colorVectorDrawable" /> android:tint="?attr/colorVectorDrawable" />
<ImageButton <ImageButton
@ -92,7 +92,7 @@
android:layout_height="48dp" android:layout_height="48dp"
android:contentDescription="@string/next" android:contentDescription="@string/next"
android:minWidth="48dp" android:minWidth="48dp"
android:src="@drawable/ic_right" android:src="@drawable/ic_arrow_end"
android:tint="?attr/colorVectorDrawable" /> android:tint="?attr/colorVectorDrawable" />
<ImageButton <ImageButton

View File

@ -1001,5 +1001,10 @@
<string name="push_notification_server_key_missing">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がありません。</string> <string name="push_notification_server_key_missing">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がありません。</string>
<string name="push_notification_server_key_empty">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がカラです。</string> <string name="push_notification_server_key_empty">サーバ側の設定ミスによりプッシュ通知は動作しません。サーバ公開鍵がカラです。</string>
<string name="divide_notification">通知を分割する (Android 6+)</string> <string name="divide_notification">通知を分割する (Android 6+)</string>
<string name="announcements">告知</string>
<string name="announcements_index">%1$d/%2$d</string>
<string name="announcements_period1">イベント期間: %1$s</string>
<string name="announcements_period2">イベント期間: %1$s%2$s</string>
<string name="edited_at">更新: %1$s</string>
</resources> </resources>

View File

@ -996,4 +996,9 @@
<string name="push_notification_server_key_missing">Push notification will not work because server misconfiguration. Server public key is missing.</string> <string name="push_notification_server_key_missing">Push notification will not work because server misconfiguration. Server public key is missing.</string>
<string name="push_notification_server_key_empty">Push notification will not work because server misconfiguration. Server public key is empty.</string> <string name="push_notification_server_key_empty">Push notification will not work because server misconfiguration. Server public key is empty.</string>
<string name="divide_notification">Divide notifications (Android 6+)</string> <string name="divide_notification">Divide notifications (Android 6+)</string>
<string name="announcements">Announcements</string>
<string name="announcements_index">%1$d/%2$d</string>
<string name="announcements_period1">Event Period: %1$s</string>
<string name="announcements_period2">Event Period: %1$s…%2$s</string>
<string name="edited_at">Edited: %1$s</string>
</resources> </resources>