added media searches, close #612

This commit is contained in:
Mariotaku Lee 2017-04-09 17:53:54 +08:00
parent 8d6d0ba601
commit 28492d2d12
No known key found for this signature in database
GPG Key ID: 15C10F89D7C33535
7 changed files with 375 additions and 121 deletions

View File

@ -0,0 +1,141 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.fragment
import android.content.Context
import android.os.Bundle
import android.support.v4.app.LoaderManager.LoaderCallbacks
import android.support.v4.app.hasRunningLoadersSafe
import android.support.v4.content.Loader
import android.support.v7.widget.StaggeredGridLayoutManager
import com.bumptech.glide.Glide
import org.mariotaku.twidere.adapter.StaggeredGridParcelableStatusesAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.constant.IntentConstants.EXTRA_FROM_USER
import org.mariotaku.twidere.extension.reachingEnd
import org.mariotaku.twidere.extension.reachingStart
import org.mariotaku.twidere.loader.iface.IExtendedLoader
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.util.IntentUtils
import org.mariotaku.twidere.view.HeaderDrawerLayout.DrawerCallback
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
/**
* Created by mariotaku on 14/11/5.
*/
abstract class AbsMediaStatusesFragment : AbsContentRecyclerViewFragment<StaggeredGridParcelableStatusesAdapter,
StaggeredGridLayoutManager>(), LoaderCallbacks<List<ParcelableStatus>?>, DrawerCallback,
IStatusViewHolder.StatusClickListener {
override final var refreshing: Boolean
get() {
if (context == null || isDetached) return false
return loaderManager.hasRunningLoadersSafe()
}
set(value) {
super.refreshing = value
}
override final val reachingEnd: Boolean
get() = layoutManager.reachingEnd
override final val reachingStart: Boolean
get() = layoutManager.reachingStart
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
adapter.statusClickListener = this
val loaderArgs = Bundle(arguments)
loaderArgs.putBoolean(EXTRA_FROM_USER, true)
loaderManager.initLoader(0, loaderArgs, this)
showProgress()
}
override final fun onCreateLayoutManager(context: Context): StaggeredGridLayoutManager {
return StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
}
override final fun scrollToPositionWithOffset(position: Int, offset: Int) {
layoutManager.scrollToPositionWithOffset(position, offset)
}
override final fun onCreateAdapter(context: Context): StaggeredGridParcelableStatusesAdapter {
return StaggeredGridParcelableStatusesAdapter(context, Glide.with(this))
}
override fun onCreateLoader(id: Int, args: Bundle): Loader<List<ParcelableStatus>?> {
val fromUser = args.getBoolean(EXTRA_FROM_USER)
args.remove(EXTRA_FROM_USER)
return onCreateStatusesLoader(activity, args, fromUser)
}
override final fun onLoadFinished(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?) {
val changed = adapter.setData(data)
if ((loader as IExtendedLoader).fromUser) {
adapter.loadMoreSupportedPosition = if (hasMoreData(loader, data, changed)) {
ILoadMoreSupportAdapter.END
} else {
ILoadMoreSupportAdapter.NONE
}
}
loader.fromUser = false
refreshing = false
showContent()
setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.NONE)
}
override final fun onLoaderReset(loader: Loader<List<ParcelableStatus>?>) {
adapter.setData(null)
}
override final fun onLoadMoreContents(position: Long) {
// Only supports load from end
if (ILoadMoreSupportAdapter.END != position) return
super.onLoadMoreContents(position)
// Get last raw status
val startIdx = adapter.statusStartIndex
if (startIdx < 0) return
val statusCount = adapter.getStatusCount(true)
if (statusCount <= 0) return
val status = adapter.getStatus(startIdx + statusCount - 1, true)
val maxId = status.id
getStatuses(maxId, null)
}
override final fun onStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)
IntentUtils.openStatus(context, status, null)
}
override final fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)
IntentUtils.openStatus(context, status.account_key, status.quoted_id)
}
protected abstract fun onCreateStatusesLoader(context: Context, args: Bundle,
fromUser: Boolean): Loader<List<ParcelableStatus>?>
protected abstract fun hasMoreData(loader: Loader<List<ParcelableStatus>?>,
data: List<ParcelableStatus>?, changed: Boolean): Boolean
protected abstract fun getStatuses(maxId: String?, sinceId: String?): Int
}

View File

@ -0,0 +1,75 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.fragment
import android.content.Context
import android.os.Bundle
import android.support.v4.content.Loader
import org.mariotaku.twidere.TwidereConstants.*
import org.mariotaku.twidere.loader.MediaStatusesSearchLoader
import org.mariotaku.twidere.loader.TweetSearchLoader
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.util.Utils
/**
* Created by mariotaku on 14/11/5.
*/
class MediaStatusesSearchFragment : AbsMediaStatusesFragment() {
override fun onCreateStatusesLoader(context: Context, args: Bundle, fromUser: Boolean):
Loader<List<ParcelableStatus>?> {
refreshing = true
val accountKey = Utils.getAccountKey(context, args)
val maxId = args.getString(EXTRA_MAX_ID)
val sinceId = args.getString(EXTRA_SINCE_ID)
val page = args.getInt(EXTRA_PAGE, -1)
val query = args.getString(EXTRA_QUERY)
val tabPosition = args.getInt(EXTRA_TAB_POSITION, -1)
val makeGap = args.getBoolean(EXTRA_MAKE_GAP, true)
val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false)
return TweetSearchLoader(activity, accountKey, query, sinceId, maxId, page, adapter.getData(),
null, tabPosition, fromUser, makeGap, loadingMore)
}
override fun hasMoreData(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?,
changed: Boolean): Boolean {
if (loader !is MediaStatusesSearchLoader) return false
val maxId = loader.maxId?.takeIf(String::isNotEmpty)
val sinceId = loader.sinceId?.takeIf(String::isNotEmpty)
if (sinceId != null && maxId != null) {
if (data != null && !data.isEmpty()) {
return changed
}
}
return false
}
override fun getStatuses(maxId: String?, sinceId: String?): Int {
if (context == null) return -1
val args = Bundle(arguments)
args.putBoolean(EXTRA_MAKE_GAP, false)
args.putString(EXTRA_MAX_ID, maxId)
args.putString(EXTRA_SINCE_ID, sinceId)
args.putBoolean(EXTRA_FROM_USER, true)
loaderManager.restartLoader(0, args, this)
return 0
}
}

View File

@ -39,6 +39,7 @@ import org.mariotaku.twidere.activity.LinkHandlerActivity
import org.mariotaku.twidere.activity.QuickSearchBarActivity
import org.mariotaku.twidere.activity.iface.IControlBarActivity.ControlBarOffsetListener
import org.mariotaku.twidere.adapter.SupportTabsAdapter
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.getAccountType
import org.mariotaku.twidere.fragment.iface.IBaseFragment.SystemWindowsInsetsCallback
import org.mariotaku.twidere.fragment.iface.RefreshScrollTopInterface
@ -56,6 +57,18 @@ class SearchFragment : AbsToolbarTabPagesFragment(), RefreshScrollTopInterface,
SystemWindowsInsetsCallback, ControlBarOffsetListener, OnPageChangeListener,
LinkHandlerActivity.HideUiOnScroll {
val accountKey: UserKey
get() = arguments.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)
val query: String
get() = arguments.getString(EXTRA_QUERY)
private val accountType: String?
get() {
val am = AccountManager.get(context)
return accountKey.let { AccountUtils.findByAccountKey(am, it) }?.getAccountType(am)
}
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
setHasOptionsMenu(true)
@ -67,10 +80,7 @@ class SearchFragment : AbsToolbarTabPagesFragment(), RefreshScrollTopInterface,
val values = ContentValues()
values.put(SearchHistory.QUERY, query)
context.contentResolver.insert(SearchHistory.CONTENT_URI, values)
val am = AccountManager.get(context)
Analyzer.log(Search(query, accountKey.let {
AccountUtils.findByAccountKey(am, it)
}?.getAccountType(am)))
Analyzer.log(Search(query, accountType))
}
val activity = this.activity
@ -144,18 +154,19 @@ class SearchFragment : AbsToolbarTabPagesFragment(), RefreshScrollTopInterface,
return false
}
val accountKey: UserKey
get() = arguments.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)!!
val query: String
get() = arguments.getString(EXTRA_QUERY)!!
override fun addTabs(adapter: SupportTabsAdapter) {
adapter.add(cls = StatusesSearchFragment::class.java, args = arguments,
name = getString(R.string.search_type_statuses), icon = DrawableHolder.resource(R.drawable.ic_action_twitter))
name = getString(R.string.search_type_statuses),
icon = DrawableHolder.resource(R.drawable.ic_action_twitter))
if (accountType == AccountType.TWITTER) {
adapter.add(cls = MediaStatusesSearchFragment::class.java, args = arguments,
name = getString(R.string.search_type_media),
icon = DrawableHolder.resource(R.drawable.ic_action_gallery))
}
adapter.add(cls = SearchUsersFragment::class.java, args = arguments,
name = getString(R.string.search_type_users), icon = DrawableHolder.resource(R.drawable.ic_action_user))
name = getString(R.string.search_type_users),
icon = DrawableHolder.resource(R.drawable.ic_action_user))
}
companion object {

View File

@ -40,9 +40,8 @@ open class StatusesSearchFragment : ParcelableStatusesFragment() {
@TimelineType
override val timelineType: String = TimelineType.SEARCH
override fun onCreateStatusesLoader(context: Context,
args: Bundle,
fromUser: Boolean): Loader<List<ParcelableStatus>?> {
override fun onCreateStatusesLoader(context: Context, args: Bundle, fromUser: Boolean):
Loader<List<ParcelableStatus>?> {
refreshing = true
val accountKey = Utils.getAccountKey(context, args)
val maxId = args.getString(EXTRA_MAX_ID)

View File

@ -2,136 +2,44 @@ package org.mariotaku.twidere.fragment
import android.content.Context
import android.os.Bundle
import android.support.v4.app.LoaderManager.LoaderCallbacks
import android.support.v4.app.hasRunningLoadersSafe
import android.support.v4.content.Loader
import android.support.v7.widget.RecyclerView
import android.support.v7.widget.StaggeredGridLayoutManager
import android.text.TextUtils
import com.bumptech.glide.Glide
import org.mariotaku.twidere.adapter.StaggeredGridParcelableStatusesAdapter
import org.mariotaku.twidere.adapter.iface.ILoadMoreSupportAdapter
import org.mariotaku.twidere.constant.IntentConstants.*
import org.mariotaku.twidere.extension.reachingEnd
import org.mariotaku.twidere.extension.reachingStart
import org.mariotaku.twidere.loader.MediaTimelineLoader
import org.mariotaku.twidere.loader.iface.IExtendedLoader
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.IntentUtils
import org.mariotaku.twidere.view.HeaderDrawerLayout.DrawerCallback
import org.mariotaku.twidere.view.holder.iface.IStatusViewHolder
/**
* Created by mariotaku on 14/11/5.
*/
class UserMediaTimelineFragment : AbsContentRecyclerViewFragment<StaggeredGridParcelableStatusesAdapter,
StaggeredGridLayoutManager>(), LoaderCallbacks<List<ParcelableStatus>>, DrawerCallback,
IStatusViewHolder.StatusClickListener {
class UserMediaTimelineFragment : AbsMediaStatusesFragment() {
override var refreshing: Boolean
get() {
if (context == null || isDetached) return false
return loaderManager.hasRunningLoadersSafe()
}
set(value) {
super.refreshing = value
}
override val reachingEnd: Boolean
get() = layoutManager.reachingEnd
override val reachingStart: Boolean
get() = layoutManager.reachingStart
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
adapter.statusClickListener = this
val loaderArgs = Bundle(arguments)
loaderArgs.putBoolean(EXTRA_FROM_USER, true)
loaderManager.initLoader(0, loaderArgs, this)
showProgress()
}
override fun setupRecyclerView(context: Context, recyclerView: RecyclerView) {
}
override fun onCreateLayoutManager(context: Context): StaggeredGridLayoutManager {
return StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)
}
override fun scrollToPositionWithOffset(position: Int, offset: Int) {
layoutManager.scrollToPositionWithOffset(position, offset)
}
override fun onCreateAdapter(context: Context): StaggeredGridParcelableStatusesAdapter {
return StaggeredGridParcelableStatusesAdapter(context, Glide.with(this))
}
override fun onCreateLoader(id: Int, args: Bundle): Loader<List<ParcelableStatus>> {
val context = activity
override fun onCreateStatusesLoader(context: Context, args: Bundle, fromUser: Boolean):
Loader<List<ParcelableStatus>?> {
val accountKey = args.getParcelable<UserKey>(EXTRA_ACCOUNT_KEY)
val maxId = args.getString(EXTRA_MAX_ID)
val sinceId = args.getString(EXTRA_SINCE_ID)
val userKey = args.getParcelable<UserKey>(EXTRA_USER_KEY)
val screenName = args.getString(EXTRA_SCREEN_NAME)
val tabPosition = args.getInt(EXTRA_TAB_POSITION, -1)
val fromUser = args.getBoolean(EXTRA_FROM_USER)
val loadingMore = args.getBoolean(EXTRA_LOADING_MORE, false)
return MediaTimelineLoader(context, accountKey, userKey, screenName, sinceId, maxId,
adapter.getData(), null, tabPosition, fromUser, loadingMore)
}
override fun onLoadFinished(loader: Loader<List<ParcelableStatus>>, data: List<ParcelableStatus>?) {
val changed = adapter.setData(data)
if ((loader as IExtendedLoader).fromUser && loader is MediaTimelineLoader) {
val maxId = loader.maxId
val sinceId = loader.sinceId
if (TextUtils.isEmpty(sinceId) && !TextUtils.isEmpty(maxId)) {
if (data != null && !data.isEmpty()) {
adapter.loadMoreSupportedPosition = if (changed) ILoadMoreSupportAdapter.END else ILoadMoreSupportAdapter.NONE
}
} else {
adapter.loadMoreSupportedPosition = ILoadMoreSupportAdapter.END
override fun hasMoreData(loader: Loader<List<ParcelableStatus>?>, data: List<ParcelableStatus>?,
changed: Boolean): Boolean {
if (loader !is MediaTimelineLoader) return false
val maxId = loader.maxId?.takeIf(String::isNotEmpty)
val sinceId = loader.sinceId?.takeIf(String::isNotEmpty)
if (sinceId != null && maxId != null) {
if (data != null && !data.isEmpty()) {
return changed
}
}
loader.fromUser = false
refreshing = false
showContent()
setLoadMoreIndicatorPosition(ILoadMoreSupportAdapter.NONE)
return false
}
override fun onLoaderReset(loader: Loader<List<ParcelableStatus>>) {
adapter.setData(null)
}
override fun onLoadMoreContents(position: Long) {
// Only supports load from end
if (ILoadMoreSupportAdapter.END != position) return
super.onLoadMoreContents(position)
// Get last raw status
val startIdx = adapter.statusStartIndex
if (startIdx < 0) return
val statusCount = adapter.getStatusCount(true)
if (statusCount <= 0) return
val status = adapter.getStatus(startIdx + statusCount - 1, true)
val maxId = status.id
getStatuses(maxId, null)
}
override fun onStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)
IntentUtils.openStatus(context, status, null)
}
override fun onQuotedStatusClick(holder: IStatusViewHolder, position: Int) {
val status = adapter.getStatus(position)
IntentUtils.openStatus(context, status.account_key, status.quoted_id)
}
fun getStatuses(maxId: String?, sinceId: String?): Int {
override fun getStatuses(maxId: String?, sinceId: String?): Int {
if (context == null) return -1
val args = Bundle(arguments)
args.putBoolean(EXTRA_MAKE_GAP, false)

View File

@ -0,0 +1,119 @@
/*
* Twidere - Twitter client for Android
*
* Copyright (C) 2012-2017 Mariotaku Lee <mariotaku.lee@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.mariotaku.twidere.loader
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.support.annotation.WorkerThread
import org.mariotaku.ktextension.isNullOrEmpty
import org.mariotaku.microblog.library.MicroBlog
import org.mariotaku.microblog.library.MicroBlogException
import org.mariotaku.microblog.library.twitter.model.Paging
import org.mariotaku.microblog.library.twitter.model.SearchQuery
import org.mariotaku.microblog.library.twitter.model.Status
import org.mariotaku.microblog.library.twitter.model.UniversalSearchQuery
import org.mariotaku.twidere.annotation.AccountType
import org.mariotaku.twidere.extension.model.official
import org.mariotaku.twidere.model.AccountDetails
import org.mariotaku.twidere.model.ParcelableStatus
import org.mariotaku.twidere.model.UserKey
import org.mariotaku.twidere.util.InternalTwitterContentUtils
open class MediaStatusesSearchLoader(
context: Context,
accountKey: UserKey?,
private val query: String?,
sinceId: String?,
maxId: String?,
page: Int,
adapterData: List<ParcelableStatus>?,
savedStatusesArgs: Array<String>?,
tabPosition: Int,
fromUser: Boolean,
override val isGapEnabled: Boolean,
loadingMore: Boolean
) : MicroBlogAPIStatusesLoader(context, accountKey, sinceId, maxId, page, adapterData, savedStatusesArgs,
tabPosition, fromUser, loadingMore) {
@Throws(MicroBlogException::class)
override fun getStatuses(microBlog: MicroBlog,
details: AccountDetails,
paging: Paging): List<Status> {
if (query == null) throw MicroBlogException("Empty query")
val queryText = processQuery(details, query)
when (details.type) {
AccountType.TWITTER -> {
if (details.extras?.official ?: false) {
val universalQuery = UniversalSearchQuery(queryText)
universalQuery.setModules(UniversalSearchQuery.Module.TWEET)
universalQuery.setResultType(UniversalSearchQuery.ResultType.RECENT)
universalQuery.setPaging(paging)
val searchResult = microBlog.universalSearch(universalQuery)
return searchResult.modules.mapNotNull { it.status?.data }
}
val searchQuery = SearchQuery(queryText)
searchQuery.paging(paging)
return microBlog.search(searchQuery)
}
}
throw MicroBlogException("Not implemented")
}
protected open fun processQuery(details: AccountDetails, query: String): String {
if (details.type == AccountType.TWITTER) {
if (details.extras?.official ?: false) {
return smQuery(query)
}
return "$query exclude:retweets filter:media"
}
return query
}
protected fun smQuery(query: String): String {
var universalQueryText = "$query filter:media"
if (maxId != null) {
universalQueryText += " max_id:$maxId"
}
if (sinceId != null) {
universalQueryText += " since_id:$sinceId"
}
return universalQueryText
}
@WorkerThread
override fun shouldFilterStatus(database: SQLiteDatabase, status: ParcelableStatus): Boolean {
if (status.media.isNullOrEmpty()) return true
return InternalTwitterContentUtils.isFiltered(database, status, true)
}
override fun processPaging(details: AccountDetails, loadItemLimit: Int, paging: Paging) {
if (details.type == AccountType.STATUSNET) {
paging.setRpp(loadItemLimit)
val page = page
if (page > 0) {
paging.setPage(page)
}
} else {
super.processPaging(details, loadItemLimit, paging)
}
}
}

View File

@ -1021,6 +1021,7 @@
<string name="search_hint_users">Search users</string>
<string name="search_statuses">Search Tweets</string>
<string name="search_type_statuses">Tweets</string>
<string name="search_type_media">Media</string>
<string name="search_type_users">Users</string>
<string name="security_key">Security key</string>