diff --git a/app/build.gradle b/app/build.gradle index 3c0110a..10eed5b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -45,6 +45,7 @@ dependencies { implementation 'com.android.support:appcompat-v7:28.0.0' implementation 'com.google.android.material:material:1.12.0' implementation 'androidx.preference:preference-ktx:1.2.1' + implementation 'androidx.activity:activity:1.8.0' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.5.2' androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1' @@ -53,4 +54,5 @@ dependencies { implementation 'androidx.media3:media3-ui:1.3.1' implementation 'androidx.media3:media3-exoplayer-hls:1.3.1' implementation "androidx.media3:media3-session:1.3.1" + implementation 'com.google.code.gson:gson:2.8.8' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 3f6ff3d..ffb7824 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -13,6 +13,11 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/Theme.P2play"> + + + tab.text = adapter.get(position) + }.attach() + } + + override fun onResume() { + super.onResume() + getChannelInfo() + } + + private fun getChannelInfo() { + CoroutineScope(Dispatchers.IO).launch { + val account = client.get(accountId) + + withContext(Dispatchers.Main) { + binding.collapsingToolbar.title = account.displayName + adapter.account = account + if (account.avatars.size > 0) { + Picasso.get().load("https://${ManagerSingleton.url}${account.avatars.last().path}").into(binding.profileImage) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/adapters/ChannelAdapter.kt b/app/src/main/java/org/libre/agosto/p2play/adapters/ChannelAdapter.kt new file mode 100644 index 0000000..9ea0467 --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/adapters/ChannelAdapter.kt @@ -0,0 +1,130 @@ +package org.libre.agosto.p2play.adapters + +import android.content.Context +import android.content.Intent +import android.os.AsyncTask +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import androidx.fragment.app.FragmentActivity +import androidx.recyclerview.widget.RecyclerView +import com.squareup.picasso.Picasso +import org.libre.agosto.p2play.AccountActivity +import org.libre.agosto.p2play.ChannelActivity +import org.libre.agosto.p2play.ManagerSingleton +import org.libre.agosto.p2play.R +import org.libre.agosto.p2play.ajax.Actions +import org.libre.agosto.p2play.models.ChannelModel + +class ChannelAdapter(private val myDataset: ArrayList): RecyclerView.Adapter() { + private val actionsService = Actions() + lateinit var parent: FragmentActivity + + class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) { + // Define click listener for the ViewHolder's View + val channelImage: ImageView = view.findViewById(R.id.channelImage) + val channelName: TextView = view.findViewById(R.id.channelName) + val channelDescription: TextView = view.findViewById(R.id.channelDescription) + val subscribeButton: Button = view.findViewById(R.id.subscribeBtn) + val context: Context = view.context + var isSubscribed: Boolean = false + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChannelAdapter.ViewHolder { + val view = LayoutInflater.from(parent.context) + .inflate(R.layout.view_channel, parent, false) as View + + return ViewHolder(view) + } + + override fun getItemCount() = myDataset.size + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + holder.channelName.text = myDataset[position].name + holder.channelDescription.text = myDataset[position].description + + if (myDataset[position].channelImg != "") { + Picasso.get().load("https://${ManagerSingleton.url}${myDataset[position].channelImg}").into(holder.channelImage) + } else { + holder.channelImage.setImageResource(R.drawable.default_avatar) + } + + if (ManagerSingleton.user.status == 1) { + getSubscription(myDataset[position], holder) + } else { + holder.subscribeButton.visibility = View.GONE + } + + holder.subscribeButton.setOnClickListener { + this.subscribeAction(myDataset[position], holder) + } + + holder.channelImage.setOnClickListener { + this.launchChannelActivity(myDataset[position].getAccount()) + } + holder.channelName.setOnClickListener { + this.launchChannelActivity(myDataset[position].getAccount()) + } + } + + private fun getSubscription(channel: ChannelModel, holder: ViewHolder) { + AsyncTask.execute { + holder.isSubscribed = actionsService.getSubscription(ManagerSingleton.token.token, channel.getAccount()) + parent.runOnUiThread { + if (holder.isSubscribed) { + holder.subscribeButton.text = parent.getText(R.string.unSubscribeBtn) + } else { + holder.subscribeButton.text = parent.getText(R.string.subscribeBtn) + } + } + } + } + + private fun subscribe(channel: ChannelModel, holder: ViewHolder) { + AsyncTask.execute { + val res = actionsService.subscribe(ManagerSingleton.token.token, channel.getAccount()) + parent.runOnUiThread { + if (res == 1) { + holder.subscribeButton.text = parent.getString(R.string.unSubscribeBtn) + ManagerSingleton.toast(parent.getString(R.string.subscribeMsg), parent) + getSubscription(channel, holder) + } else { + ManagerSingleton.toast(parent.getString(R.string.errorMsg), parent) + } + } + } + } + + private fun unSubscribe(channel: ChannelModel, holder: ViewHolder) { + AsyncTask.execute { + val res = actionsService.unSubscribe(ManagerSingleton.token.token, channel.getAccount()) + parent.runOnUiThread { + if (res == 1) { + holder.subscribeButton.text = parent.getString(R.string.subscribeBtn) + ManagerSingleton.toast(parent.getString(R.string.unSubscribeMsg), parent) + getSubscription(channel, holder) + } else { + ManagerSingleton.toast(parent.getString(R.string.errorMsg), parent) + } + } + } + } + + private fun subscribeAction(channel: ChannelModel, holder: ViewHolder) { + if (holder.isSubscribed) { + unSubscribe(channel, holder) + } else { + subscribe(channel, holder) + } + } + + private fun launchChannelActivity (channelId: String) { + val intent = Intent(parent, ChannelActivity::class.java) + intent.putExtra("channel", channelId) + parent.startActivity(intent) + + } +} \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/adapters/CommentariesAdapter.kt b/app/src/main/java/org/libre/agosto/p2play/adapters/CommentariesAdapter.kt index 92a8add..0fcf03d 100644 --- a/app/src/main/java/org/libre/agosto/p2play/adapters/CommentariesAdapter.kt +++ b/app/src/main/java/org/libre/agosto/p2play/adapters/CommentariesAdapter.kt @@ -1,6 +1,7 @@ package org.libre.agosto.p2play.adapters import android.content.Context +import android.content.Intent import android.os.Bundle import android.text.Html import android.view.LayoutInflater @@ -12,6 +13,7 @@ import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentTransaction import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso +import org.libre.agosto.p2play.AccountActivity import org.libre.agosto.p2play.ManagerSingleton import org.libre.agosto.p2play.R import org.libre.agosto.p2play.dialogs.ThreadDialog @@ -86,11 +88,11 @@ class CommentariesAdapter(private val myDataset: ArrayList) : } // TODO: Support for view and account (is different than a video channel) - // holder.userImg.setOnClickListener { - // val intent = Intent(holder.context, ChannelActivity::class.java) - // intent.putExtra("channel", myDataset[position].getAccount()) - // holder.context.startActivity(intent) - // } + holder.userImg.setOnClickListener { + val intent = Intent(holder.context, AccountActivity::class.java) + intent.putExtra("accountId", myDataset[position].getAccount()) + holder.context.startActivity(intent) + } } // Return the size of your dataset (invoked by the layout manager) diff --git a/app/src/main/java/org/libre/agosto/p2play/adapters/VideosAdapter.kt b/app/src/main/java/org/libre/agosto/p2play/adapters/VideosAdapter.kt index 4f4b68c..417fbd1 100644 --- a/app/src/main/java/org/libre/agosto/p2play/adapters/VideosAdapter.kt +++ b/app/src/main/java/org/libre/agosto/p2play/adapters/VideosAdapter.kt @@ -9,6 +9,7 @@ import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView import com.squareup.picasso.Picasso +import org.libre.agosto.p2play.AccountActivity import org.libre.agosto.p2play.ChannelActivity import org.libre.agosto.p2play.ManagerSingleton import org.libre.agosto.p2play.R diff --git a/app/src/main/java/org/libre/agosto/p2play/ajax/Accounts.kt b/app/src/main/java/org/libre/agosto/p2play/ajax/Accounts.kt new file mode 100644 index 0000000..2f5099c --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/ajax/Accounts.kt @@ -0,0 +1,58 @@ +package org.libre.agosto.p2play.ajax + +import android.util.JsonReader +import com.google.gson.Gson +import org.libre.agosto.p2play.models.AccountModel +import org.libre.agosto.p2play.models.ChannelModel +import org.libre.agosto.p2play.models.VideoModel +import java.io.InputStreamReader + +class Accounts : Client() { + + fun get(accountId: String): AccountModel { + val con = this.newCon("accounts/$accountId", "GET") + lateinit var account: AccountModel + try { + if (con.responseCode == 200) { + val response = InputStreamReader(con.inputStream) + account = Gson().fromJson(response, AccountModel::class.java) + } + } catch (err: Exception) { + err.printStackTrace() + } + + return account + } + + fun getChannels(accountId: String): ArrayList { + val con = this.newCon("accounts/$accountId/video-channels", "GET") + val channels = arrayListOf() + try { + if (con.responseCode == 200) { + val response = InputStreamReader(con.inputStream) + val data = JsonReader(response) + data.beginObject() + while (data.hasNext()) { + when (data.nextName()) { + "data" -> { + data.beginArray() + while (data.hasNext()) { + val channel = ChannelModel() + channel.parseChannel(data) + channels.add(channel) + } + data.endArray() + } + else -> data.skipValue() + } + } + data.endObject() + data.close() + } + } catch (err: Exception) { + err.printStackTrace() + } + + return channels + } +} diff --git a/app/src/main/java/org/libre/agosto/p2play/ajax/Channels.kt b/app/src/main/java/org/libre/agosto/p2play/ajax/Channels.kt index 30ca885..e12f82b 100644 --- a/app/src/main/java/org/libre/agosto/p2play/ajax/Channels.kt +++ b/app/src/main/java/org/libre/agosto/p2play/ajax/Channels.kt @@ -6,14 +6,6 @@ import java.io.InputStreamReader class Channels : Client() { - private fun parseChannel(data: JsonReader): ChannelModel { - val channel = ChannelModel() - - data.close() - - return channel - } - fun getChannelInfo(account: String): ChannelModel { val con = this.newCon("video-channels/$account", "GET") var channel = ChannelModel() diff --git a/app/src/main/java/org/libre/agosto/p2play/ajax/Videos.kt b/app/src/main/java/org/libre/agosto/p2play/ajax/Videos.kt index 9965ca7..34fd495 100644 --- a/app/src/main/java/org/libre/agosto/p2play/ajax/Videos.kt +++ b/app/src/main/java/org/libre/agosto/p2play/ajax/Videos.kt @@ -218,4 +218,24 @@ class Videos : Client() { con.disconnect() return video } + + fun accountVideos(account: String, start: Int): ArrayList { + val count = ManagerSingleton.videosCount + val params = "start=$start&count=$count" + val con = this.newCon("accounts/$account/videos?$params", "GET") + var videos = arrayListOf() + try { + if (con.responseCode == 200) { + val response = InputStreamReader(con.inputStream) + val data = JsonReader(response) + videos = parseVideos(data) + data.close() + } + } catch (err: Exception) { + err.printStackTrace() + } + + con.disconnect() + return videos + } } diff --git a/app/src/main/java/org/libre/agosto/p2play/fragmentAdapters/AccountAdapter.kt b/app/src/main/java/org/libre/agosto/p2play/fragmentAdapters/AccountAdapter.kt new file mode 100644 index 0000000..dd04ea5 --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/fragmentAdapters/AccountAdapter.kt @@ -0,0 +1,38 @@ +package org.libre.agosto.p2play.fragmentAdapters + +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Lifecycle +import androidx.viewpager2.adapter.FragmentStateAdapter +import org.libre.agosto.p2play.fragments.AccountVideosFragment +import org.libre.agosto.p2play.fragments.AccountChannelsFragment +import org.libre.agosto.p2play.fragments.AccountInfoFragment +import org.libre.agosto.p2play.models.AccountModel + +val TAB_NAMES = arrayOf( + "Videos", + "Channels", + "Info" +) + +class AccountAdapter(fm: FragmentManager, c: Lifecycle) : FragmentStateAdapter(fm, c) { + lateinit var accountId: String + lateinit var account: AccountModel + override fun getItemCount(): Int = 3 + + override fun createFragment(i: Int): Fragment { + val fragment = when (i) { + 0 -> AccountVideosFragment.newInstance(accountId) + 1 -> AccountChannelsFragment.newInstance(accountId) + 2 -> AccountInfoFragment.newInstance(account) + else -> throw Error("Invalid tab") + } + + return fragment + } + + fun get(position: Int): CharSequence { + return TAB_NAMES[position] + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/fragments/AccountChannelsFragment.kt b/app/src/main/java/org/libre/agosto/p2play/fragments/AccountChannelsFragment.kt new file mode 100644 index 0000000..dd26e68 --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/fragments/AccountChannelsFragment.kt @@ -0,0 +1,82 @@ +package org.libre.agosto.p2play.fragments + +import android.os.AsyncTask +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.libre.agosto.p2play.adapters.ChannelAdapter +import org.libre.agosto.p2play.ajax.Accounts +import org.libre.agosto.p2play.databinding.FragmentChannelsBinding +import org.libre.agosto.p2play.helpers.getViewManager +import org.libre.agosto.p2play.models.ChannelModel + +private const val ARG_PARAM1 = "accountId" + +class AccountChannelsFragment : Fragment() { + private var _binding: FragmentChannelsBinding? = null + private val binding get() = _binding!! + private var accountId: String? = null + + private val client = Accounts() + + private lateinit var viewManager: RecyclerView.LayoutManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + accountId = it.getString(ARG_PARAM1) + } + viewManager = getViewManager(this.requireContext(), resources) + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentChannelsBinding.inflate(inflater, container, false) + + return binding.root + } + + override fun onResume() { + super.onResume() + getChannels() + } + + private fun getChannels() { + AsyncTask.execute { + val channels = client.getChannels(this.accountId!!) + requireActivity().runOnUiThread { + initRecycler(channels) + } + } + } + + private fun initRecycler(data: ArrayList) { + val viewAdapter = ChannelAdapter(data) + + binding.channelList.apply { + viewAdapter.parent = requireActivity() + setHasFixedSize(true) + + // use a linear layout manager + layoutManager = viewManager + + // specify an viewAdapter (see also next example) + adapter = viewAdapter + } + } + + companion object { + @JvmStatic + fun newInstance(accountId: String) = + AccountChannelsFragment().apply { + arguments = Bundle().apply { + putString(ARG_PARAM1, accountId) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/fragments/AccountInfoFragment.kt b/app/src/main/java/org/libre/agosto/p2play/fragments/AccountInfoFragment.kt new file mode 100644 index 0000000..c0480c5 --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/fragments/AccountInfoFragment.kt @@ -0,0 +1,50 @@ +package org.libre.agosto.p2play.fragments + +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import org.libre.agosto.p2play.databinding.FragmentChannelInfoBinding +import org.libre.agosto.p2play.models.AccountModel +import java.io.Serializable + +private const val ARG_PARAM1 = "account" + +class AccountInfoFragment : Fragment() { + private var _binding: FragmentChannelInfoBinding? = null + private val binding get() = _binding!! + private var account: AccountModel? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + account = it.getSerializable(ARG_PARAM1) as AccountModel + } + } + + override fun onCreateView( + inflater: LayoutInflater, container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentChannelInfoBinding.inflate(inflater, container, false) + + binding.account.text = account?.name + binding.host.text = account?.host + binding.description.text = account?.description + + return binding.root + } + + companion object { + + @JvmStatic + fun newInstance(account: AccountModel) = + AccountInfoFragment().apply { + arguments = Bundle().apply { + val param = account as Serializable + putSerializable(ARG_PARAM1, param) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/fragments/AccountVideosFragment.kt b/app/src/main/java/org/libre/agosto/p2play/fragments/AccountVideosFragment.kt new file mode 100644 index 0000000..9d9b04b --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/fragments/AccountVideosFragment.kt @@ -0,0 +1,86 @@ +package org.libre.agosto.p2play.fragments + +import android.os.AsyncTask +import android.os.Bundle +import androidx.fragment.app.Fragment +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import org.libre.agosto.p2play.adapters.VideosAdapter +import org.libre.agosto.p2play.ajax.Videos +import org.libre.agosto.p2play.databinding.FragmentChannelVideosBinding +import org.libre.agosto.p2play.helpers.getViewManager +import org.libre.agosto.p2play.models.VideoModel + +private const val ARG_PARAM1 = "accountId" + +class AccountVideosFragment : Fragment() { + private var _binding: FragmentChannelVideosBinding? = null + private val binding get() = _binding!! + + private var accountId: String? = "agosto182" + private val videosService = Videos() + + private lateinit var viewManager: RecyclerView.LayoutManager + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + arguments?.let { + accountId = it.getString(ARG_PARAM1, accountId) + } + + viewManager = getViewManager(this.requireContext(), resources) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + _binding = FragmentChannelVideosBinding.inflate(inflater, container, false) + return binding.root + } + + override fun onResume() { + super.onResume() + getVideos() + } + + private fun getVideos() { + AsyncTask.execute { + val videos = videosService.accountVideos(this.accountId!!, 0) + activity?.runOnUiThread { + initRecycler(videos) + } + } + } + + private fun initRecycler(data: ArrayList) { + // val data = arrayListOf() + val viewAdapter = VideosAdapter(data) + + binding.videosList.apply { + // use this setting to improve performance if you know that changes + // in content do not change the layout size of the RecyclerView + setHasFixedSize(true) + + // use a linear layout manager + layoutManager = viewManager + + // specify an viewAdapter (see also next example) + adapter = viewAdapter + } + } + + companion object { + + @JvmStatic + fun newInstance(param1: String) = + AccountVideosFragment().apply { + arguments = Bundle().apply { + putString(ARG_PARAM1, param1) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/libre/agosto/p2play/models/AccountModel.kt b/app/src/main/java/org/libre/agosto/p2play/models/AccountModel.kt new file mode 100644 index 0000000..b0d7f29 --- /dev/null +++ b/app/src/main/java/org/libre/agosto/p2play/models/AccountModel.kt @@ -0,0 +1,16 @@ +package org.libre.agosto.p2play.models + +import java.io.Serializable + +data class AccountAvatar ( + val path: String +) + +class AccountModel ( + val name: String, + val url: String, + val host: String, + val avatars: ArrayList, + val displayName: String, + val description: String +): Serializable \ No newline at end of file diff --git a/app/src/main/res/layout/activity_account.xml b/app/src/main/res/layout/activity_account.xml new file mode 100644 index 0000000..f43e129 --- /dev/null +++ b/app/src/main/res/layout/activity_account.xml @@ -0,0 +1,72 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_info.xml b/app/src/main/res/layout/fragment_channel_info.xml new file mode 100644 index 0000000..d6b1f01 --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_info.xml @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channel_videos.xml b/app/src/main/res/layout/fragment_channel_videos.xml new file mode 100644 index 0000000..a60d459 --- /dev/null +++ b/app/src/main/res/layout/fragment_channel_videos.xml @@ -0,0 +1,12 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_channels.xml b/app/src/main/res/layout/fragment_channels.xml new file mode 100644 index 0000000..22154ee --- /dev/null +++ b/app/src/main/res/layout/fragment_channels.xml @@ -0,0 +1,13 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_channel.xml b/app/src/main/res/layout/view_channel.xml new file mode 100644 index 0000000..e530110 --- /dev/null +++ b/app/src/main/res/layout/view_channel.xml @@ -0,0 +1,53 @@ + + + + + + + + + +