(試験対応)トレンドタグと検索v2

This commit is contained in:
tateisu 2018-05-30 14:18:45 +09:00
parent 640fee04ac
commit 4d5173808f
16 changed files with 403 additions and 30 deletions

View File

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

View File

@ -1008,6 +1008,12 @@ class ActMain : AppCompatActivity()
false,
Column.TYPE_FOLLOW_REQUESTS
)
R.id.nav_add_trend_tag ->Action_Account.timeline(
this,
defaultInsertPosition,
true,
Column.TYPE_TREND_TAG
)
// トゥート検索
R.id.mastodon_search_portal -> addColumn(

View File

@ -95,6 +95,7 @@ class Column(
private const val PATH_STATUSES = "/api/v1/statuses/%d" // 1:status_id
private const val PATH_STATUSES_CONTEXT = "/api/v1/statuses/%d/context" // 1:status_id
const val PATH_SEARCH = "/api/v1/search?q=%s"
const val PATH_SEARCH_V2 = "/api/v2/search?q=%s"
// search args 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts
private const val PATH_INSTANCE = "/api/v1/instance"
private const val PATH_LIST_INFO = "/api/v1/lists/%s"
@ -164,6 +165,7 @@ class Column(
internal const val TYPE_LIST_MEMBER = 21
internal const val TYPE_SEARCH_TS = 22
internal const val TYPE_DIRECT_MESSAGES = 23
internal const val TYPE_TREND_TAG = 24
internal const val TAB_STATUS = 0
internal const val TAB_FOLLOWING = 1
@ -210,6 +212,7 @@ class Column(
TYPE_LIST_MEMBER -> context.getString(R.string.list_member)
TYPE_LIST_TL -> context.getString(R.string.list_timeline)
TYPE_DIRECT_MESSAGES -> context.getString(R.string.direct_messages)
TYPE_TREND_TAG ->context.getString(R.string.trend_tag)
else -> "?"
}
}
@ -237,6 +240,7 @@ class Column(
TYPE_LIST_MEMBER -> R.attr.ic_list_member
TYPE_LIST_TL -> R.attr.ic_list_tl
TYPE_DIRECT_MESSAGES -> R.attr.ic_mail
TYPE_TREND_TAG -> R.attr.ic_hashtag
else -> R.attr.ic_info
}
}
@ -1878,11 +1882,57 @@ class Column(
return result
}
TYPE_TREND_TAG ->{
result = client.request( "/api/v1/trends" )
val src = parser.trendTagList(result?.jsonArray)
src.sortBy { - it.history.first().uses }
this.list_tmp = addAll(this.list_tmp, src)
return result
}
TYPE_SEARCH -> {
if(access_info.isPseudo) {
// 1.5.0rc からマストドンの検索APIは認証を要求するようになった
return TootApiResult(context.getString(R.string.search_is_not_available_on_pseudo_account))
}
var instance = access_info.instance
if( instance == null ){
// まだ取得してない
val r2 = getInstanceInformation(client, null)
if(instance_tmp != null) {
instance = instance_tmp
access_info.instance = instance
}
}
if( instance != null && instance.versionGE(TootInstance.VERSION_2_4_0) ){
// v2 api を試す
var path = String.format(
Locale.JAPAN,
PATH_SEARCH_V2,
search_query.encodePercent()
)
if(search_resolve) path += "&resolve=1"
result = client.request(path)
val jsonObject = result?.jsonObject
if( jsonObject != null ){
val tmp = parser.resultsV2(jsonObject)
if(tmp != null) {
list_tmp = ArrayList()
addAll(list_tmp, tmp.hashtags)
addAll(list_tmp, tmp.accounts)
addAll(list_tmp, tmp.statuses)
}
return result
}
}
var path =
String.format(
Locale.JAPAN,
@ -3510,7 +3560,12 @@ class Column(
fun canReloadWhenRefreshTop() : Boolean {
return when(column_type) {
TYPE_SEARCH, TYPE_SEARCH_MSP, TYPE_SEARCH_TS, TYPE_CONVERSATION, TYPE_LIST_LIST -> true
TYPE_SEARCH,
TYPE_SEARCH_MSP,
TYPE_SEARCH_TS,
TYPE_CONVERSATION,
TYPE_LIST_LIST ,
TYPE_TREND_TAG-> true
else -> false
}
}

View File

@ -484,10 +484,10 @@ class ColumnViewHolder(
when(column.column_type) {
Column.TYPE_CONVERSATION, Column.TYPE_INSTANCE_INFORMATION -> refreshLayout.isEnabled =
false
Column.TYPE_CONVERSATION,
Column.TYPE_INSTANCE_INFORMATION -> refreshLayout.isEnabled = false
Column.TYPE_SEARCH -> {
Column.TYPE_SEARCH, Column.TYPE_TREND_TAG -> {
refreshLayout.isEnabled = true
refreshLayout.direction = SwipyRefreshLayoutDirection.TOP
}

View File

@ -106,6 +106,11 @@ internal class ItemViewHolder(
private lateinit var llSearchTag : View
private lateinit var btnSearchTag : Button
private lateinit var llTrendTag : View
private lateinit var tvTrendTagName : TextView
private lateinit var tvTrendTagDesc : TextView
private lateinit var tvTrendTagCount : TextView
private lateinit var cvTrendTagHistory : TrendTagHistoryView
private lateinit var llList : View
private lateinit var btnListTL : Button
@ -181,6 +186,8 @@ internal class ItemViewHolder(
btnHideMedia.setOnClickListener(this)
llTrendTag.setOnClickListener(this)
llTrendTag.setOnLongClickListener(this)
this.content_color_default = tvContent.textColors.defaultColor
@ -196,6 +203,8 @@ internal class ItemViewHolder(
tvApplication.textSize = activity.timeline_font_size_sp
tvMessageHolder.textSize = activity.timeline_font_size_sp
btnListTL.textSize = activity.timeline_font_size_sp
tvTrendTagName.textSize = activity.timeline_font_size_sp
tvTrendTagCount.textSize = activity.timeline_font_size_sp
}
if(! activity.acct_font_size_sp.isNaN()) {
@ -204,6 +213,7 @@ internal class ItemViewHolder(
tvFollowerAcct.textSize = activity.acct_font_size_sp
tvAcct.textSize = activity.acct_font_size_sp
tvTime.textSize = activity.acct_font_size_sp
tvTrendTagDesc.textSize = activity.acct_font_size_sp
}
ivThumbnail.layoutParams.height = activity.avatarIconSize
@ -241,7 +251,11 @@ internal class ItemViewHolder(
// ボタンは太字なので触らない
} else if(v is TextView) {
val typeface = when {
v === tvName || v === tvFollowerName || v === tvBoosted -> activity.timeline_font_bold
v === tvName ||
v === tvFollowerName ||
v === tvBoosted ||
v === tvTrendTagCount ||
v === tvTrendTagName -> activity.timeline_font_bold
?: activity.timeline_font
else -> activity.timeline_font ?: activity.timeline_font_bold
}
@ -321,6 +335,7 @@ internal class ItemViewHolder(
llFollowRequest.visibility = View.GONE
llExtra.removeAllViews()
tvMessageHolder.visibility = View.GONE
llTrendTag.visibility = View.GONE
var c : Int
c = if(column.content_color != 0) column.content_color else content_color_default
@ -333,6 +348,9 @@ internal class ItemViewHolder(
//NSFWは文字色固定 btnShowMedia.setTextColor( c );
tvApplication.setTextColor(c)
tvMessageHolder.setTextColor(c)
tvTrendTagName.setTextColor(c)
tvTrendTagCount.setTextColor(c)
cvTrendTagHistory.setColor(c)
c = if(column.acct_color != 0) column.acct_color else Styler.getAttributeColor(
activity,
@ -341,6 +359,7 @@ internal class ItemViewHolder(
this.acct_color = c
tvBoostedTime.setTextColor(c)
tvTime.setTextColor(c)
tvTrendTagDesc.setTextColor(c)
// tvBoostedAcct.setTextColor( c );
// tvFollowerAcct.setTextColor( c );
// tvAcct.setTextColor( c );
@ -366,18 +385,30 @@ internal class ItemViewHolder(
is TootNotification -> showNotification(item)
is TootTag -> showSearchTag(item)
is TootGap -> showGap()
is TootDomainBlock -> showDomainBlock(item)
is TootList -> showList(item)
is TootMessageHolder -> showMessageHolder(item)
// TootTrendTag の後に TootTagを判定すること
is TootTrendTag -> showTrendTag(item)
is TootTag -> showSearchTag(item)
else -> {
}
}
}
private fun showTrendTag(item : TootTrendTag) {
llTrendTag.visibility = View.VISIBLE
tvTrendTagName.text = "#${item.name}"
val latest = item.history.first()
tvTrendTagDesc.text = activity.getString(R.string.people_talking, latest.accounts)
tvTrendTagCount.text = latest.uses.toString()
cvTrendTagHistory.setHistory(item.history)
}
private fun showMessageHolder(item : TootMessageHolder) {
tvMessageHolder.visibility = View.VISIBLE
tvMessageHolder.text = item.text
@ -945,7 +976,7 @@ internal class ItemViewHolder(
DlgContextMenu(activity, column, who, null, notification).show()
}
btnSearchTag -> when(item) {
btnSearchTag, llTrendTag -> when(item) {
is TootGap -> column.startGap(item)
is TootDomainBlock -> {
@ -1098,7 +1129,7 @@ internal class ItemViewHolder(
return true
}
btnSearchTag -> {
btnSearchTag, llTrendTag -> {
val item = this.item
when(item) {
// is TootGap -> column.startGap(item)
@ -1907,6 +1938,36 @@ internal class ItemViewHolder(
}.lparams(matchParent, wrapContent)
}
llTrendTag = linearLayout {
lparams(matchParent, wrapContent)
gravity = Gravity.CENTER_VERTICAL
background = ContextCompat.getDrawable(context, R.drawable.btn_bg_transparent)
verticalLayout {
lparams(0, wrapContent) {
weight = 1f
}
tvTrendTagName = textView {
}.lparams(matchParent, wrapContent)
tvTrendTagDesc = textView {
textColor = Styler.getAttributeColor(context, R.attr.colorTimeSmall)
textSize = 12f // SP
}.lparams(matchParent, wrapContent)
}
tvTrendTagCount = textView {
}.lparams(wrapContent, wrapContent) {
startMargin = dip(6)
endMargin = dip(6)
}
cvTrendTagHistory = trendTagHistoryView {
}.lparams(dip(64), dip(32))
}
llList = linearLayout {
lparams(matchParent, wrapContent)

View File

@ -28,5 +28,8 @@ class TootParser(
fun results(src : JSONObject?) = parseItem(::TootResults, this, src)
fun instance(src : JSONObject?) = parseItem(::TootInstance, this, src)
fun trendTagList(array : JSONArray?)= parseList(::TootTrendTag, array)
fun resultsV2(src : JSONObject) = parseItem(::TootResultsV2, this, src)
}

View File

@ -13,6 +13,7 @@ class TootInstance(parser : TootParser, src : JSONObject) {
val VERSION_1_6 = VersionString("1.6")
val VERSION_2_4_0_rc1 = VersionString("2.4.0rc1")
val VERSION_2_4_0_rc2 = VersionString("2.4.0rc2")
val VERSION_2_4_0 = VersionString("2.4.0")
}

View File

@ -0,0 +1,20 @@
package jp.juggler.subwaytooter.api.entity
import org.json.JSONObject
import java.util.ArrayList
import jp.juggler.subwaytooter.api.TootParser
class TootResultsV2(
val accounts : ArrayList<TootAccountRef>, // An array of matched Accounts
val statuses : ArrayList<TootStatus>, // An array of matched Statuses
val hashtags : ArrayList<TootTrendTag> // An array of matched hashtags
) {
constructor(parser : TootParser, src : JSONObject) : this(
accounts = parser.accountList(src.optJSONArray("accounts")),
statuses = parser.statusList( src.optJSONArray("statuses")),
hashtags = parser.trendTagList(src.optJSONArray("hashtags"))
)
}

View File

@ -6,14 +6,13 @@ import jp.juggler.subwaytooter.util.parseString
import org.json.JSONArray
import org.json.JSONObject
class TootTag(
open class TootTag(
// The hashtag, not including the preceding #
val name : String,
// The URL of the hashtag. may null if generated from TootContext
val url : String? = null
) : TimelineItem() {
constructor(src : JSONObject) : this(
name = src.notEmptyOrThrow("name"),
url = src.parseString("url")

View File

@ -0,0 +1,60 @@
package jp.juggler.subwaytooter.api.entity
import jp.juggler.subwaytooter.util.LogCategory
import jp.juggler.subwaytooter.util.notEmptyOrThrow
import jp.juggler.subwaytooter.util.parseLong
import jp.juggler.subwaytooter.util.parseString
import org.json.JSONArray
import org.json.JSONObject
class TootTrendTag(
name : String,
url : String?,
val history : ArrayList<History>
) : TootTag(name, url) {
class History(src : JSONObject) {
val day : Long
val uses : Long
val accounts : Long
init {
day = src.parseLong("day")
?: throw RuntimeException("TootTrendTag.History: missing day")
uses = src.parseLong("uses")
?: throw RuntimeException("TootTrendTag.History: missing uses")
accounts = src.parseLong("accounts")
?: throw RuntimeException("TootTrendTag.History: missing accounts")
}
}
constructor(src : JSONObject) : this(
name = src.notEmptyOrThrow("name"),
url = src.parseString("url"),
history = parseHistory(src.optJSONArray("history"))
)
companion object {
val log = LogCategory("TootTrendTag")
private fun parseHistory(src : JSONArray?) : ArrayList<History> {
src ?: throw RuntimeException("TootTrendTag: missing history")
val dst = ArrayList<History>()
for(i in 0 until src.length()) {
try {
dst.add(History(src.optJSONObject(i)))
} catch(ex : Throwable) {
log.e(ex, "history parse failed.")
}
}
if(dst.isEmpty()) {
throw RuntimeException("TootTrendTag: empty history")
}
return dst
}
}
}

View File

@ -3,6 +3,7 @@ package jp.juggler.subwaytooter.util
import android.view.ViewManager
import jp.juggler.subwaytooter.view.MyNetworkImageView
import jp.juggler.subwaytooter.view.MyTextView
import jp.juggler.subwaytooter.view.TrendTagHistoryView
import org.jetbrains.anko.custom.ankoView
// Anko Layout中にカスタムビューを指定する為に拡張関数を定義する
@ -11,7 +12,12 @@ inline fun ViewManager.myNetworkImageView(init: MyNetworkImageView.() -> Unit):
return ankoView({ MyNetworkImageView(it) }, theme = 0, init = init)
}
inline fun ViewManager.myTextView(init: MyTextView.() -> Unit): MyTextView {
inline fun ViewManager.myTextView(init: MyTextView.() -> Unit) : MyTextView {
return ankoView({ MyTextView(it) }, theme = 0, init = init)
}
inline fun ViewManager.trendTagHistoryView(init: TrendTagHistoryView.() -> Unit): TrendTagHistoryView {
return ankoView({ TrendTagHistoryView(it) }, theme = 0, init = init)
}

View File

@ -0,0 +1,153 @@
package jp.juggler.subwaytooter.view
import android.content.Context
import android.graphics.*
import android.util.AttributeSet
import android.view.View
import android.view.animation.PathInterpolator
import jp.juggler.subwaytooter.api.entity.TootTrendTag
import jp.juggler.subwaytooter.util.clipRange
class TrendTagHistoryView : View {
private val paint = Paint()
private var values : List<Float>? = null
private var delta : Float = 0f
private val path = Path()
private var lineWidth = 1f
private var y_workarea : Array<Float>? = null
constructor(context : Context) : super(context) {
init()
}
constructor(context : Context, attrs : AttributeSet?) : super(context, attrs) {
init()
}
constructor(context : Context, attrs : AttributeSet?, defStyleAttr : Int) : super(
context,
attrs,
defStyleAttr
) {
init()
}
private fun init() {
val density = context.resources.displayMetrics.density
this.lineWidth = 2f * density
paint.style = Paint.Style.STROKE
paint.strokeWidth = lineWidth
}
fun setColor(c : Int) {
paint.color = c
invalidate()
}
fun setHistory(history : ArrayList<TootTrendTag.History>?) {
if(history?.isEmpty() != false) {
delta = 0f
values = null
} else {
var min = Long.MAX_VALUE
var max = Long.MIN_VALUE
for(h in history) {
min = Math.min(min, h.uses)
max = Math.max(max, h.uses)
}
val delta = (max - min).toFloat()
this.delta = delta
if(delta == 0f) {
values = null
} else {
values = history.map { (it.uses - min).toFloat() / delta }.reversed()
y_workarea = Array(history.size) { 0f }
}
}
invalidate()
}
override fun onDraw(canvas : Canvas) {
super.onDraw(canvas)
val values = this.values ?: return
val view_w = width.toFloat()
val view_h = height.toFloat()
if(view_w < 1f || view_h < 1f) return
if(delta == 0f) {
val y = height / 2f
canvas.drawLine(0f, y, view_w, y, paint)
return
}
val size = values.size
val x_step = view_w / (size - 1).toFloat()
var x = 0f
path.reset()
var lastSlope = 0f
var lastY = 0f
var lastX = 0f
val controlXStep = x_step / 2f
val y_workarea = this.y_workarea ?: return
val y_min = lineWidth * 2f
val y_max = view_h - lineWidth * 2f
val y_width = y_max - y_min
for(i in 0 until size) {
y_workarea[i] = (1f - values[i]) * y_width + y_min
}
for(i in 0 until size) {
val y = y_workarea[i]
when(i) {
0 -> {
path.moveTo(x, y)
lastSlope = (y_workarea[i + 1] - y) / x_step
}
size - 1 -> {
// 制御点1
val c1x = lastX + controlXStep
val c1y = clipRange(y_min, y_max, lastY + controlXStep * lastSlope)
// 制御点2
val slope = (y - lastY) / x_step
val c2x = x - controlXStep
val c2y = y - controlXStep * slope
path.cubicTo(c1x, c1y, c2x, c2y, x, y)
}
else -> {
// 制御点1
val c1x = lastX + controlXStep
val c1y = clipRange(y_min, y_max, lastY + controlXStep * lastSlope)
// 制御点2
val nextY = y_workarea[i + 1]
val slope = if((y > lastY && y > nextY) || (y < lastY && y < nextY)) {
// 極値は傾き0とみなす
0f
} else if(y == lastY || y == nextY) {
// 左右のどちらかが平坦なら平坦とみなす
0f
} else {
// 前後で同じように勾配しているなら傾きは平均とする
val slope1 = (y - lastY) / x_step
val slope2 = (nextY - y) / x_step
(slope1 + slope2) / 2f
}
val c2x = x - controlXStep
val c2y = clipRange(y_min, y_max, y - controlXStep * slope)
path.cubicTo(c1x, c1y, c2x, c2y, x, y)
lastSlope = slope
}
}
lastX = x
lastY = y
x += x_step
}
canvas.drawPath(path, paint)
}
}

View File

@ -4,17 +4,17 @@
<group android:checkableBehavior="single">
<item android:title="@string/account">
<menu>
<menu>
<item
android:id="@+id/nav_account_add"
android:icon="?attr/ic_account_add"
android:title="@string/account_add"/>
<item
android:id="@+id/nav_account_setting"
android:icon="?attr/ic_setting"
android:title="@string/account_setting"/>
</menu>
<item
android:id="@+id/nav_account_add"
android:icon="?attr/ic_account_add"
android:title="@string/account_add"/>
<item
android:id="@+id/nav_account_setting"
android:icon="?attr/ic_setting"
android:title="@string/account_setting"/>
</menu>
</item>
<item android:title="@string/column">
<menu>
@ -31,6 +31,7 @@
android:id="@+id/nav_add_notifications"
android:icon="?attr/btn_notification"
android:title="@string/notifications"/>
<item
android:id="@+id/nav_add_direct_message"
android:icon="?attr/ic_mail"
@ -46,7 +47,6 @@
android:icon="?attr/btn_federate_tl"
android:title="@string/federate_timeline"/>
<item
android:id="@+id/nav_add_list"
android:icon="?attr/ic_list_list"
@ -57,6 +57,11 @@
android:icon="?attr/ic_search"
android:title="@string/search"/>
<item
android:id="@+id/nav_add_trend_tag"
android:icon="?attr/ic_hashtag"
android:title="@string/trend_tag"/>
<item
android:id="@+id/nav_add_favourites"
android:icon="?attr/btn_favourite"
@ -92,7 +97,6 @@
<!--android:icon="?attr/btn_report"-->
<!--android:title="@string/your_reports"/>-->
<!--<item-->
<!--android:id="@+id/nav_slideshow"-->
<!--android:icon="?attr/ic_menu_slideshow"-->
@ -116,7 +120,6 @@
</menu>
</item>
<item android:title="@string/setting">
<menu>
@ -149,9 +152,9 @@
android:title="@string/app_about"/>
<!--<item-->
<!--android:id="@+id/nav_translation"-->
<!--android:icon="?attr/ic_info"-->
<!--android:title="@string/help_translation"/>-->
<!--android:id="@+id/nav_translation"-->
<!--android:icon="?attr/ic_info"-->
<!--android:title="@string/help_translation"/>-->
<item
android:id="@+id/nav_oss_license"

View File

@ -678,8 +678,10 @@
<string name="none_or_hidden_followers">There is no followers, or hidden by setting.</string>
<string name="yourself_can_see_your_network">Even if you choose to hide social graphs, yourself can see it.</string>
<string name="follow_follower_list_may_restrict">If remote user chooses to hide the social graph, only the followings/followers on this instance will be displayed.</string>
<string name="trend_tag">Trending tags</string>
<string name="people_talking">%1$d people talking</string>
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description">Revenir à l\'accueil</string>-->
<!--<string name="abc_action_bar_home_description_format">%1$s, %2$s</string>-->
<!--<string name="abc_action_bar_home_subtitle_description_format">%1$s, %2$s, %3$s</string>-->
<!--<string name="abc_action_bar_up_description">Revenir en haut de la page</string>-->

View File

@ -956,5 +956,7 @@
<string name="none_or_hidden_followers">フォロワーはいません。または設定により隠されています。</string>
<string name="yourself_can_see_your_network">もしあなたがソーシャルグラフを隠す選択をした場合でも、あなた自身はそれを見ることができます。</string>
<string name="follow_follower_list_may_restrict">もしリモートのユーザがソーシャルグラフを隠す選択をした場合、表示されるのはこのタンスのユーザだけです。</string>
<string name="trend_tag">トレンドタグ</string>
<string name="people_talking">%1$d人がトゥート</string>
</resources>

View File

@ -663,4 +663,6 @@
<string name="none_or_hidden_followers">There is no followers, or hidden by setting.</string>
<string name="yourself_can_see_your_network">Even if you choose to hide social graphs, yourself can see it.</string>
<string name="follow_follower_list_may_restrict">If remote user chooses to hide the social graph, only the followings/followers on this instance will be displayed.</string>
<string name="trend_tag">Trending tags</string>
<string name="people_talking">%1$d people talking</string>
</resources>