2017-11-05 22:32:36 +01:00
|
|
|
/* Copyright 2017 Andrew Dawson
|
|
|
|
*
|
|
|
|
* This file is a part of Tusky.
|
|
|
|
*
|
|
|
|
* 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.
|
|
|
|
*
|
|
|
|
* Tusky 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 Tusky; if not,
|
|
|
|
* see <http://www.gnu.org/licenses>. */
|
|
|
|
|
|
|
|
package com.keylesspalace.tusky.fragment
|
|
|
|
|
|
|
|
import android.graphics.Color
|
|
|
|
import android.os.Bundle
|
|
|
|
import android.util.Log
|
|
|
|
import android.view.LayoutInflater
|
|
|
|
import android.view.View
|
|
|
|
import android.view.ViewGroup
|
|
|
|
import android.widget.ImageView
|
2019-01-21 08:37:25 +01:00
|
|
|
import androidx.core.app.ActivityOptionsCompat
|
|
|
|
import androidx.core.content.ContextCompat
|
|
|
|
import androidx.core.view.ViewCompat
|
2018-12-17 15:25:35 +01:00
|
|
|
import androidx.recyclerview.widget.GridLayoutManager
|
|
|
|
import androidx.recyclerview.widget.RecyclerView
|
2019-04-16 21:39:12 +02:00
|
|
|
import com.bumptech.glide.Glide
|
2017-11-05 22:32:36 +01:00
|
|
|
import com.keylesspalace.tusky.R
|
|
|
|
import com.keylesspalace.tusky.ViewMediaActivity
|
2018-03-27 19:47:00 +02:00
|
|
|
import com.keylesspalace.tusky.di.Injectable
|
2017-11-30 20:12:09 +01:00
|
|
|
import com.keylesspalace.tusky.entity.Attachment
|
2017-11-05 22:32:36 +01:00
|
|
|
import com.keylesspalace.tusky.entity.Status
|
2019-05-15 12:43:16 +02:00
|
|
|
import com.keylesspalace.tusky.interfaces.RefreshableFragment
|
2017-11-05 22:32:36 +01:00
|
|
|
import com.keylesspalace.tusky.network.MastodonApi
|
2018-01-20 13:39:01 +01:00
|
|
|
import com.keylesspalace.tusky.util.ThemeUtils
|
2019-01-31 18:31:15 +01:00
|
|
|
import com.keylesspalace.tusky.util.hide
|
|
|
|
import com.keylesspalace.tusky.util.show
|
2017-11-05 22:32:36 +01:00
|
|
|
import com.keylesspalace.tusky.view.SquareImageView
|
2018-05-10 20:13:25 +02:00
|
|
|
import com.keylesspalace.tusky.viewdata.AttachmentViewData
|
2018-06-17 21:49:51 +02:00
|
|
|
import kotlinx.android.synthetic.main.fragment_timeline.*
|
2017-11-05 22:32:36 +01:00
|
|
|
import retrofit2.Call
|
|
|
|
import retrofit2.Callback
|
|
|
|
import retrofit2.Response
|
2019-01-31 18:31:15 +01:00
|
|
|
import java.io.IOException
|
2017-11-05 22:32:36 +01:00
|
|
|
import java.util.*
|
2018-03-27 19:47:00 +02:00
|
|
|
import javax.inject.Inject
|
2017-11-05 22:32:36 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Created by charlag on 26/10/2017.
|
|
|
|
*
|
|
|
|
* Fragment with multiple columns of media previews for the specified account.
|
|
|
|
*/
|
|
|
|
|
2019-05-15 12:43:16 +02:00
|
|
|
class AccountMediaFragment : BaseFragment(), RefreshableFragment, Injectable {
|
2017-11-05 22:32:36 +01:00
|
|
|
companion object {
|
|
|
|
@JvmStatic
|
2019-05-15 12:43:16 +02:00
|
|
|
fun newInstance(accountId: String, enableSwipeToRefresh:Boolean=true): AccountMediaFragment {
|
2017-11-05 22:32:36 +01:00
|
|
|
val fragment = AccountMediaFragment()
|
2017-11-09 14:35:52 +01:00
|
|
|
val args = Bundle()
|
|
|
|
args.putString(ACCOUNT_ID_ARG, accountId)
|
2019-05-15 12:43:16 +02:00
|
|
|
args.putBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,enableSwipeToRefresh)
|
2017-11-09 14:35:52 +01:00
|
|
|
fragment.arguments = args
|
2017-11-05 22:32:36 +01:00
|
|
|
return fragment
|
|
|
|
}
|
|
|
|
|
|
|
|
private const val ACCOUNT_ID_ARG = "account_id"
|
|
|
|
private const val TAG = "AccountMediaFragment"
|
2019-05-15 12:43:16 +02:00
|
|
|
private const val ARG_ENABLE_SWIPE_TO_REFRESH = "arg.enable.swipe.to.refresh"
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
|
2019-05-15 12:43:16 +02:00
|
|
|
private var isSwipeToRefreshEnabled: Boolean = true
|
|
|
|
private var needToRefresh = false
|
|
|
|
|
2018-03-27 19:47:00 +02:00
|
|
|
@Inject
|
|
|
|
lateinit var api: MastodonApi
|
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
private val adapter = MediaGridAdapter()
|
|
|
|
private var currentCall: Call<List<Status>>? = null
|
|
|
|
private val statuses = mutableListOf<Status>()
|
|
|
|
private var fetchingStatus = FetchingStatus.NOT_FETCHING
|
2019-01-28 11:05:13 +01:00
|
|
|
private var isVisibleToUser: Boolean = false
|
2017-11-05 22:32:36 +01:00
|
|
|
|
2019-09-22 08:18:44 +02:00
|
|
|
private lateinit var accountId: String
|
2019-05-15 12:43:16 +02:00
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
private val callback = object : Callback<List<Status>> {
|
|
|
|
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
|
|
|
|
fetchingStatus = FetchingStatus.NOT_FETCHING
|
2019-02-12 19:22:37 +01:00
|
|
|
|
2019-04-16 21:39:12 +02:00
|
|
|
if (isAdded) {
|
2019-02-12 19:22:37 +01:00
|
|
|
swipeRefreshLayout.isRefreshing = false
|
|
|
|
progressBar.visibility = View.GONE
|
2019-05-15 12:43:16 +02:00
|
|
|
topProgressBar?.hide()
|
2019-01-31 18:31:15 +01:00
|
|
|
statusView.show()
|
|
|
|
if (t is IOException) {
|
|
|
|
statusView.setup(R.drawable.elephant_offline, R.string.error_network) {
|
|
|
|
doInitialLoadingIfNeeded()
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
statusView.setup(R.drawable.elephant_error, R.string.error_generic) {
|
|
|
|
doInitialLoadingIfNeeded()
|
|
|
|
}
|
|
|
|
}
|
2018-07-08 11:41:08 +02:00
|
|
|
}
|
2018-06-17 21:49:51 +02:00
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
Log.d(TAG, "Failed to fetch account media", t)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
|
|
|
fetchingStatus = FetchingStatus.NOT_FETCHING
|
2019-04-16 21:39:12 +02:00
|
|
|
if (isAdded) {
|
2019-02-12 19:22:37 +01:00
|
|
|
swipeRefreshLayout.isRefreshing = false
|
|
|
|
progressBar.visibility = View.GONE
|
2019-05-15 12:43:16 +02:00
|
|
|
topProgressBar?.hide()
|
2018-07-09 08:59:06 +02:00
|
|
|
|
|
|
|
val body = response.body()
|
|
|
|
body?.let { fetched ->
|
|
|
|
statuses.addAll(0, fetched)
|
|
|
|
// flatMap requires iterable but I don't want to box each array into list
|
|
|
|
val result = mutableListOf<AttachmentViewData>()
|
|
|
|
for (status in fetched) {
|
|
|
|
result.addAll(AttachmentViewData.list(status))
|
|
|
|
}
|
|
|
|
adapter.addTop(result)
|
2019-05-15 12:43:16 +02:00
|
|
|
if (result.isNotEmpty())
|
|
|
|
recyclerView.scrollToPosition(0)
|
2019-02-12 19:22:37 +01:00
|
|
|
|
2019-01-31 18:31:15 +01:00
|
|
|
if (statuses.isEmpty()) {
|
|
|
|
statusView.show()
|
|
|
|
statusView.setup(R.drawable.elephant_friend_empty, R.string.message_empty,
|
|
|
|
null)
|
|
|
|
}
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private val bottomCallback = object : Callback<List<Status>> {
|
|
|
|
override fun onFailure(call: Call<List<Status>>?, t: Throwable?) {
|
|
|
|
fetchingStatus = FetchingStatus.NOT_FETCHING
|
2019-01-31 18:31:15 +01:00
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
Log.d(TAG, "Failed to fetch account media", t)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onResponse(call: Call<List<Status>>, response: Response<List<Status>>) {
|
|
|
|
fetchingStatus = FetchingStatus.NOT_FETCHING
|
|
|
|
val body = response.body()
|
|
|
|
body?.let { fetched ->
|
|
|
|
Log.d(TAG, "fetched ${fetched.size} statuses")
|
|
|
|
if (fetched.isNotEmpty()) Log.d(TAG, "first: ${fetched.first().id}, last: ${fetched.last().id}")
|
|
|
|
statuses.addAll(fetched)
|
|
|
|
Log.d(TAG, "now there are ${statuses.size} statuses")
|
|
|
|
// flatMap requires iterable but I don't want to box each array into list
|
2018-05-10 20:13:25 +02:00
|
|
|
val result = mutableListOf<AttachmentViewData>()
|
2017-11-05 22:32:36 +01:00
|
|
|
for (status in fetched) {
|
2018-05-10 20:13:25 +02:00
|
|
|
result.addAll(AttachmentViewData.list(status))
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
adapter.addBottom(result)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
2019-05-15 12:43:16 +02:00
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
2019-09-22 08:18:44 +02:00
|
|
|
isSwipeToRefreshEnabled = arguments?.getBoolean(ARG_ENABLE_SWIPE_TO_REFRESH,true) == true
|
|
|
|
accountId = arguments?.getString(ACCOUNT_ID_ARG)!!
|
2019-05-15 12:43:16 +02:00
|
|
|
}
|
2017-11-05 22:32:36 +01:00
|
|
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
|
|
|
|
savedInstanceState: Bundle?): View? {
|
2018-06-17 21:49:51 +02:00
|
|
|
return inflater.inflate(R.layout.fragment_timeline, container, false)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
|
|
super.onViewCreated(view, savedInstanceState)
|
|
|
|
|
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
val columnCount = view.context.resources.getInteger(R.integer.profile_media_column_count)
|
|
|
|
val layoutManager = GridLayoutManager(view.context, columnCount)
|
2017-11-05 22:32:36 +01:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
val bgRes = ThemeUtils.getColorId(view.context, R.attr.window_background)
|
2018-01-20 13:39:01 +01:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
adapter.baseItemColor = ContextCompat.getColor(recyclerView.context, bgRes)
|
2017-11-05 22:32:36 +01:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
recyclerView.layoutManager = layoutManager
|
|
|
|
recyclerView.adapter = adapter
|
2017-11-05 22:32:36 +01:00
|
|
|
|
|
|
|
|
|
|
|
|
2019-05-15 12:43:16 +02:00
|
|
|
if (isSwipeToRefreshEnabled) {
|
|
|
|
swipeRefreshLayout.setOnRefreshListener {
|
|
|
|
refresh()
|
|
|
|
}
|
|
|
|
swipeRefreshLayout.setColorSchemeResources(R.color.tusky_blue)
|
|
|
|
swipeRefreshLayout.setProgressBackgroundColorSchemeColor(ThemeUtils.getColor(view.context, android.R.attr.colorBackground))
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
2019-01-31 18:31:15 +01:00
|
|
|
statusView.visibility = View.GONE
|
2017-11-05 22:32:36 +01:00
|
|
|
|
2019-02-12 19:22:37 +01:00
|
|
|
recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() {
|
2018-06-17 21:49:51 +02:00
|
|
|
|
2018-09-16 19:23:32 +02:00
|
|
|
override fun onScrolled(recycler_view: RecyclerView, dx: Int, dy: Int) {
|
2017-11-05 22:32:36 +01:00
|
|
|
if (dy > 0) {
|
|
|
|
val itemCount = layoutManager.itemCount
|
|
|
|
val lastItem = layoutManager.findLastCompletelyVisibleItemPosition()
|
|
|
|
if (itemCount <= lastItem + 3 && fetchingStatus == FetchingStatus.NOT_FETCHING) {
|
|
|
|
statuses.lastOrNull()?.let { last ->
|
|
|
|
Log.d(TAG, "Requesting statuses with max_id: ${last.id}, (bottom)")
|
|
|
|
fetchingStatus = FetchingStatus.FETCHING_BOTTOM
|
2019-01-21 08:37:25 +01:00
|
|
|
currentCall = api.accountStatuses(accountId, last.id, null, null, null, true, null)
|
2017-11-05 22:32:36 +01:00
|
|
|
currentCall?.enqueue(bottomCallback)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
})
|
2019-01-28 11:05:13 +01:00
|
|
|
|
|
|
|
if (isVisibleToUser) doInitialLoadingIfNeeded()
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
|
2019-05-15 12:43:16 +02:00
|
|
|
private fun refresh() {
|
|
|
|
statusView.hide()
|
|
|
|
if (fetchingStatus != FetchingStatus.NOT_FETCHING) return
|
|
|
|
currentCall = if (statuses.isEmpty()) {
|
|
|
|
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
|
|
|
api.accountStatuses(accountId, null, null, null, null, true, null)
|
|
|
|
} else {
|
|
|
|
fetchingStatus = FetchingStatus.REFRESHING
|
|
|
|
api.accountStatuses(accountId, null, statuses[0].id, null, null, true, null)
|
|
|
|
}
|
|
|
|
currentCall?.enqueue(callback)
|
|
|
|
|
|
|
|
if (!isSwipeToRefreshEnabled)
|
|
|
|
topProgressBar?.show()
|
|
|
|
}
|
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
// That's sort of an optimization to only load media once user has opened the tab
|
2019-01-28 11:05:13 +01:00
|
|
|
// Attention: can be called before *any* lifecycle method!
|
2017-11-05 22:32:36 +01:00
|
|
|
override fun setUserVisibleHint(isVisibleToUser: Boolean) {
|
|
|
|
super.setUserVisibleHint(isVisibleToUser)
|
2019-01-28 11:05:13 +01:00
|
|
|
this.isVisibleToUser = isVisibleToUser
|
|
|
|
if (isVisibleToUser && isAdded) doInitialLoadingIfNeeded()
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun doInitialLoadingIfNeeded() {
|
2019-01-31 18:31:15 +01:00
|
|
|
if (isAdded) {
|
|
|
|
statusView.hide()
|
|
|
|
}
|
2017-11-05 22:32:36 +01:00
|
|
|
if (fetchingStatus == FetchingStatus.NOT_FETCHING && statuses.isEmpty()) {
|
|
|
|
fetchingStatus = FetchingStatus.INITIAL_FETCHING
|
2019-01-21 08:37:25 +01:00
|
|
|
currentCall = api.accountStatuses(accountId, null, null, null, null, true, null)
|
2017-11-05 22:32:36 +01:00
|
|
|
currentCall?.enqueue(callback)
|
|
|
|
}
|
2019-05-15 12:43:16 +02:00
|
|
|
else if (needToRefresh)
|
|
|
|
refresh()
|
|
|
|
needToRefresh = false
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
private fun viewMedia(items: List<AttachmentViewData>, currentIndex: Int, view: View?) {
|
2017-11-05 22:32:36 +01:00
|
|
|
|
2019-04-20 22:36:44 +02:00
|
|
|
when (items[currentIndex].attachment.type) {
|
2018-10-15 19:56:11 +02:00
|
|
|
Attachment.Type.IMAGE,
|
|
|
|
Attachment.Type.GIFV,
|
2019-09-08 19:35:43 +02:00
|
|
|
Attachment.Type.VIDEO,
|
|
|
|
Attachment.Type.AUDIO -> {
|
2018-05-10 20:13:25 +02:00
|
|
|
val intent = ViewMediaActivity.newIntent(context, items, currentIndex)
|
2017-11-09 14:35:52 +01:00
|
|
|
if (view != null && activity != null) {
|
2018-05-10 20:13:25 +02:00
|
|
|
val url = items[currentIndex].attachment.url
|
2017-11-05 22:32:36 +01:00
|
|
|
ViewCompat.setTransitionName(view, url)
|
2017-11-09 14:35:52 +01:00
|
|
|
val options = ActivityOptionsCompat.makeSceneTransitionAnimation(activity!!, view, url)
|
2017-11-05 22:32:36 +01:00
|
|
|
startActivity(intent, options.toBundle())
|
|
|
|
} else {
|
|
|
|
startActivity(intent)
|
|
|
|
}
|
|
|
|
}
|
2018-03-03 13:24:03 +01:00
|
|
|
Attachment.Type.UNKNOWN -> {
|
2017-11-05 22:32:36 +01:00
|
|
|
}/* Intentionally do nothing. This case is here is to handle when new attachment
|
|
|
|
* types are added to the API before code is added here to handle them. So, the
|
|
|
|
* best fallback is to just show the preview and ignore requests to view them. */
|
2018-07-08 11:41:08 +02:00
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private enum class FetchingStatus {
|
|
|
|
NOT_FETCHING, INITIAL_FETCHING, FETCHING_BOTTOM, REFRESHING
|
|
|
|
}
|
|
|
|
|
2019-01-31 18:31:15 +01:00
|
|
|
inner class MediaGridAdapter :
|
2018-12-17 15:25:35 +01:00
|
|
|
RecyclerView.Adapter<MediaGridAdapter.MediaViewHolder>() {
|
2017-11-05 22:32:36 +01:00
|
|
|
|
|
|
|
var baseItemColor = Color.BLACK
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
private val items = mutableListOf<AttachmentViewData>()
|
2017-11-05 22:32:36 +01:00
|
|
|
private val itemBgBaseHSV = FloatArray(3)
|
|
|
|
private val random = Random()
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
fun addTop(newItems: List<AttachmentViewData>) {
|
2017-11-05 22:32:36 +01:00
|
|
|
items.addAll(0, newItems)
|
|
|
|
notifyItemRangeInserted(0, newItems.size)
|
|
|
|
}
|
|
|
|
|
2018-05-10 20:13:25 +02:00
|
|
|
fun addBottom(newItems: List<AttachmentViewData>) {
|
2017-11-05 22:32:36 +01:00
|
|
|
if (newItems.isEmpty()) return
|
|
|
|
|
|
|
|
val oldLen = items.size
|
|
|
|
items.addAll(newItems)
|
|
|
|
notifyItemRangeInserted(oldLen, newItems.size)
|
|
|
|
}
|
|
|
|
|
2018-06-17 21:49:51 +02:00
|
|
|
override fun onAttachedToRecyclerView(recycler_view: RecyclerView) {
|
2017-11-05 22:32:36 +01:00
|
|
|
val hsv = FloatArray(3)
|
|
|
|
Color.colorToHSV(baseItemColor, hsv)
|
2018-06-17 21:49:51 +02:00
|
|
|
super.onAttachedToRecyclerView(recycler_view)
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MediaViewHolder {
|
|
|
|
val view = SquareImageView(parent.context)
|
|
|
|
view.scaleType = ImageView.ScaleType.CENTER_CROP
|
|
|
|
return MediaViewHolder(view)
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun getItemCount(): Int = items.size
|
|
|
|
|
|
|
|
override fun onBindViewHolder(holder: MediaViewHolder, position: Int) {
|
|
|
|
itemBgBaseHSV[2] = random.nextFloat() * (1f - 0.3f) + 0.3f
|
|
|
|
holder.imageView.setBackgroundColor(Color.HSVToColor(itemBgBaseHSV))
|
|
|
|
val item = items[position]
|
2019-02-16 14:31:41 +01:00
|
|
|
|
2019-04-16 21:39:12 +02:00
|
|
|
Glide.with(holder.imageView)
|
2018-05-10 20:13:25 +02:00
|
|
|
.load(item.attachment.previewUrl)
|
2019-02-16 14:31:41 +01:00
|
|
|
.centerInside()
|
2017-11-05 22:32:36 +01:00
|
|
|
.into(holder.imageView)
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
inner class MediaViewHolder(val imageView: ImageView)
|
|
|
|
: RecyclerView.ViewHolder(imageView),
|
|
|
|
View.OnClickListener {
|
|
|
|
init {
|
|
|
|
itemView.setOnClickListener(this)
|
|
|
|
}
|
|
|
|
|
|
|
|
// saving some allocations
|
|
|
|
override fun onClick(v: View?) {
|
|
|
|
viewMedia(items, adapterPosition, imageView)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2019-05-15 12:43:16 +02:00
|
|
|
|
|
|
|
override fun refreshContent() {
|
|
|
|
if (isAdded)
|
|
|
|
refresh()
|
|
|
|
else
|
|
|
|
needToRefresh = true
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2017-11-05 22:32:36 +01:00
|
|
|
}
|