Merge branch 'develop' into 'master'

Release

See merge request sschueller/peertube!58
This commit is contained in:
Stefan Schüller 2022-01-09 12:56:23 +00:00
commit eb3b1eb7ad
16 changed files with 827 additions and 351 deletions

View File

@ -1,78 +1,83 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="net.schueller.peertube">
xmlns:tools="http://schemas.android.com/tools"
package="net.schueller.peertube">
<!-- required to play video in background via notification -->
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <!-- connect to peertube server -->
<uses-permission android:name="android.permission.INTERNET" /> <!-- required for torrent downloading -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/> <!-- connect to peertube server -->
<uses-permission android:name="android.permission.INTERNET"/> <!-- required for torrent downloading -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<application
android:name=".application.AppApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
android:name=".application.AppApplication"
android:allowBackup="true"
android:fullBackupContent="@xml/backup_descriptor"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
tools:ignore="GoogleAppIndexingWarning">
<!-- Server Address Book -->
<activity
android:name=".activity.ServerAddressBookActivity"
android:label="@string/title_activity_server_address_book"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Video Lists -->
android:name=".activity.ServerAddressBookActivity"
android:label="@string/title_activity_server_address_book"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Video Lists -->
<activity
android:name=".activity.VideoListActivity"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
android:name=".activity.VideoListActivity"
android:launchMode="singleTop"
android:theme="@style/AppTheme.NoActionBar"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<action android:name="android.intent.action.SEARCH" />
<action android:name="android.intent.action.MAIN"/>
<action android:name="android.intent.action.SEARCH"/>
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
<meta-data
android:name="android.app.searchable"
android:resource="@xml/searchable" />
android:name="android.app.searchable"
android:resource="@xml/searchable"/>
</activity> <!-- Video Player -->
<activity
android:name=".activity.VideoPlayActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:label="@string/title_activity_video_play"
android:launchMode="singleInstance"
android:supportsPictureInPicture="true"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Settings -->
android:name=".activity.VideoPlayActivity"
android:configChanges="keyboard|keyboardHidden|orientation|screenSize|screenLayout|smallestScreenSize|uiMode"
android:label="@string/title_activity_video_play"
android:launchMode="singleInstance"
android:supportsPictureInPicture="true"
android:theme="@style/AppTheme.NoActionBar"/>
<!-- Playlist -->
<activity android:name=".activity.PlaylistActivity"
android:label="Playlist"
android:theme="@style/AppTheme.NoActionBar"/>
<!-- Settings -->
<activity
android:name=".activity.SettingsActivity"
android:label="@string/title_activity_settings"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Server Selection -->
android:name=".activity.SettingsActivity"
android:label="@string/title_activity_settings"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Server Selection -->
<activity
android:name=".activity.SearchServerActivity"
android:label="@string/title_activity_select_server"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Me -->
android:name=".activity.SearchServerActivity"
android:label="@string/title_activity_select_server"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Me -->
<activity
android:name=".activity.MeActivity"
android:label="@string/title_activity_me"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Account -->
android:name=".activity.MeActivity"
android:label="@string/title_activity_me"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Account -->
<activity
android:name=".activity.AccountActivity"
android:label="@string/title_activity_account"
android:theme="@style/AppTheme.NoActionBar" /> <!-- Content provider for search suggestions -->
android:name=".activity.AccountActivity"
android:label="@string/title_activity_account"
android:theme="@style/AppTheme.NoActionBar"/> <!-- Content provider for search suggestions -->
<provider
android:name=".provider.SearchSuggestionsProvider"
android:authorities="net.schueller.peertube.provider.SearchSuggestionsProvider"
android:enabled="true"
android:exported="false" />
android:name=".provider.SearchSuggestionsProvider"
android:authorities="net.schueller.peertube.provider.SearchSuggestionsProvider"
android:enabled="true"
android:exported="false"/>
<service android:name=".service.VideoPlayerService" />
<service android:name=".service.VideoPlayerService"/>
<receiver android:name="androidx.media.session.MediaButtonReceiver"
android:exported="true">
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MEDIA_BUTTON" />
<action android:name="android.intent.action.MEDIA_BUTTON"/>
</intent-filter>
</receiver>

View File

@ -27,7 +27,9 @@ import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import com.squareup.picasso.Picasso;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
@ -36,18 +38,12 @@ import net.schueller.peertube.model.Me;
import net.schueller.peertube.network.GetUserService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.network.Session;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import com.squareup.picasso.Picasso;
import java.util.Objects;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import java.util.Objects;
import static net.schueller.peertube.application.AppApplication.getContext;
public class MeActivity extends CommonActivity {
@ -85,11 +81,16 @@ public class MeActivity extends CommonActivity {
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
LinearLayout account = findViewById(R.id.a_me_account_line);
LinearLayout playlist = findViewById(R.id.a_me_playlist);
LinearLayout settings = findViewById(R.id.a_me_settings);
LinearLayout help = findViewById(R.id.a_me_helpnfeedback);
TextView logout = findViewById(R.id.a_me_logout);
playlist.setOnClickListener(view -> {
Intent playlistActivity = new Intent(getContext(), PlaylistActivity.class);
startActivity(playlistActivity);
});
settings.setOnClickListener(view -> {
Intent settingsActivity = new Intent(getContext(), SettingsActivity.class);
@ -124,7 +125,7 @@ public class MeActivity extends CommonActivity {
call.enqueue(new Callback<Me>() {
LinearLayout account = findViewById(R.id.a_me_account_line);
final LinearLayout account = findViewById(R.id.a_me_account_line);
@Override
public void onResponse(@NonNull Call<Me> call, @NonNull Response<Me> response) {
@ -162,7 +163,7 @@ public class MeActivity extends CommonActivity {
@Override
public void onFailure(@NonNull Call<Me> call, @NonNull Throwable t) {
ErrorHelper.showToastFromCommunicationError( MeActivity.this, t );
ErrorHelper.showToastFromCommunicationError(MeActivity.this, t);
account.setVisibility(View.GONE);
}
});

View File

@ -0,0 +1,103 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.adapter.MultiViewRecyclerViewHolder
import net.schueller.peertube.adapter.PlaylistAdapter
import net.schueller.peertube.database.Video
import net.schueller.peertube.database.VideoViewModel
import net.schueller.peertube.databinding.ActivityPlaylistBinding
class PlaylistActivity : CommonActivity() {
private val TAG = "PlaylistAct"
private val mVideoViewModel: VideoViewModel by viewModels()
private lateinit var mBinding: ActivityPlaylistBinding
override fun onSupportNavigateUp(): Boolean {
finish() // close this activity as oppose to navigating up
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityPlaylistBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(mBinding.toolBarServerAddressBook)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
}
showServers()
}
private fun onVideoClick(video: Video) {
val intent = Intent(this, VideoPlayActivity::class.java)
intent.putExtra(MultiViewRecyclerViewHolder.EXTRA_VIDEOID, video.videoUUID)
startActivity(intent)
}
private fun showServers() {
val adapter = PlaylistAdapter(mutableListOf(), { onVideoClick(it) }).also {
mBinding.serverListRecyclerview.adapter = it
}
// Delete items on swipe
val helper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
AlertDialog.Builder(this@PlaylistActivity)
.setTitle(getString(R.string.remove_video))
.setMessage(getString(R.string.remove_video_warning_message))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val position = viewHolder.bindingAdapterPosition
val video = adapter.getVideoAtPosition(position)
// Delete the video
mVideoViewModel.delete(video)
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
})
helper.attachToRecyclerView(mBinding.serverListRecyclerview)
// Update the cached copy of the words in the adapter.
mVideoViewModel.allVideos.observe(this, { videos: List<Video> ->
adapter.setVideos(videos)
})
}
companion object
}

View File

@ -30,28 +30,26 @@ import androidx.core.content.ContextCompat
import androidx.recyclerview.widget.RecyclerView
import androidx.viewbinding.ViewBinding
import com.google.gson.JsonObject
import com.mikepenz.iconics.Iconics.Builder
import com.squareup.picasso.Picasso
import net.schueller.peertube.R.color
import net.schueller.peertube.R.string
import net.schueller.peertube.R
import net.schueller.peertube.R.*
import net.schueller.peertube.activity.AccountActivity
import net.schueller.peertube.activity.VideoListActivity
import net.schueller.peertube.activity.VideoPlayActivity
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.MetaDataHelper.getCreatorAvatar
import net.schueller.peertube.helper.MetaDataHelper.getCreatorString
import net.schueller.peertube.helper.MetaDataHelper.getDuration
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import com.mikepenz.iconics.Iconics.Builder
import net.schueller.peertube.R
import net.schueller.peertube.R.id
import net.schueller.peertube.R.menu
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.helper.MetaDataHelper.getCreatorAvatar
import net.schueller.peertube.helper.MetaDataHelper.getCreatorString
import net.schueller.peertube.helper.MetaDataHelper.getTagsString
import net.schueller.peertube.helper.MetaDataHelper.isChannel
import net.schueller.peertube.intents.Intents
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetUserService
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.network.Session
@ -61,8 +59,6 @@ import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import net.schueller.peertube.helper.MetaDataHelper.isChannel
import net.schueller.peertube.network.GetUserService
sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
@ -70,13 +66,13 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
var videoRating: Rating? = null
var isLeaveAppExpected = false
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding): MultiViewRecyclerViewHolder(binding) {
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(category: Category) {
binding.textViewTitle.text = category.label
}
}
class VideoCommentsViewHolder(private val binding: ItemVideoCommentsOverviewBinding): MultiViewRecyclerViewHolder(binding) {
class VideoCommentsViewHolder(private val binding: ItemVideoCommentsOverviewBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(commentThread: CommentThread) {
binding.videoCommentsTotalCount.text = commentThread.total.toString()
@ -90,8 +86,8 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
val baseUrl = APIUrlHelper.getUrl(binding.videoHighlightedAvatar.context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.videoHighlightedAvatar)
.load(baseUrl + avatarPath)
.into(binding.videoHighlightedAvatar)
}
binding.videoHighlightedComment.text = highlightedComment.text
}
@ -99,7 +95,7 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
}
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?): MultiViewRecyclerViewHolder(binding) {
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?) : MultiViewRecyclerViewHolder(binding) {
fun bind(videoMetaViewItem: VideoMetaViewItem) {
val video = videoMetaViewItem.video
@ -109,16 +105,16 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
val context = binding.avatar.context
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
GetVideoDataService::class.java
)
val userService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetUserService::class.java
GetUserService::class.java
)
// Title
@ -137,27 +133,25 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
rateVideo(false, video, context, binding)
}
// Add to playlist
binding.videoAddToPlaylistWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
videoMetaDataFragment.saveToPlaylist(video)
Toast.makeText(context, context.getString(string.saved_to_playlist), Toast.LENGTH_SHORT).show()
}
binding.videoBlockWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
binding.videoFlagWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).show()
}
@ -198,10 +192,10 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
// created at / views
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context,
true
video.createdAt,
video.views,
context,
true
)
// owner / creator
@ -220,8 +214,8 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
val baseUrl = APIUrlHelper.getUrl(context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// videoOwnerSubscribers
@ -243,7 +237,6 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
}
// get subscription status
var isSubscribed = false
@ -262,6 +255,7 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
}
}
}
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
// Do nothing.
}
@ -278,52 +272,54 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
val call = userService.subscribe(body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
isSubscribed = true
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
} else {
AlertDialog.Builder(context)
.setTitle(context.getString(string.video_sub_del_alert_title))
.setMessage(context.getString(string.video_sub_del_alert_msg))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// Yes
val payload = video.channel.name + "@" + video.channel.host
val call = userService.unsubscribe(payload)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
isSubscribed = false
.setTitle(context.getString(string.video_sub_del_alert_title))
.setMessage(context.getString(string.video_sub_del_alert_msg))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// Yes
val payload = video.channel.name + "@" + video.channel.host
val call = userService.unsubscribe(payload)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
isSubscribed = false
}
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
// No
}
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
// No
}
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
@ -333,7 +329,7 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
}
}
class ChannelViewHolder(private val binding: ItemChannelTitleBinding): MultiViewRecyclerViewHolder(binding) {
class ChannelViewHolder(private val binding: ItemChannelTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(channel: Channel) {
val context = binding.avatar.context
@ -344,22 +340,22 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.placeholder(R.drawable.test_image)
.into(binding.avatar)
.load(baseUrl + avatarPath)
.placeholder(R.drawable.test_image)
.into(binding.avatar)
}
binding.textViewTitle.text = channel.displayName
}
}
class TagViewHolder(private val binding: ItemTagTitleBinding): MultiViewRecyclerViewHolder(binding) {
class TagViewHolder(private val binding: ItemTagTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(tag: TagVideo) {
binding.textViewTitle.text = tag.tag
}
}
class VideoViewHolder(private val binding: RowVideoListBinding): MultiViewRecyclerViewHolder(binding) {
class VideoViewHolder(private val binding: RowVideoListBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(video: Video) {
@ -368,18 +364,18 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
// Temp Loading Image
Picasso.get()
.load(baseUrl + video.previewPath)
.placeholder(R.drawable.test_image)
.error(R.drawable.test_image)
.into(binding.thumb)
.load(baseUrl + video.previewPath)
.placeholder(R.drawable.test_image)
.error(R.drawable.test_image)
.into(binding.thumb)
// Avatar
val avatar = getCreatorAvatar(video, context)
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// set Name
binding.slRowName.text = video.name
@ -395,9 +391,9 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
// set age and view count
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context
video.createdAt,
video.views,
context
)
// set owner
@ -431,8 +427,8 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
binding.moreButton.setOnClickListener { v: View? ->
val popup = PopupMenu(
context,
v!!
context,
v!!
)
popup.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
@ -492,17 +488,17 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL, APIUrlHelper.useInsecureConnection(
apiBaseURL, APIUrlHelper.useInsecureConnection(
context
)
)
).create(
GetVideoDataService::class.java
GetVideoDataService::class.java
)
val call = videoDataService.rateVideo(video.id, body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
// if 20x, update likes/dislikes
if (response.isSuccessful) {
@ -539,17 +535,17 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Toast.makeText(
context,
context.getString(string.video_rating_failed),
Toast.LENGTH_SHORT
context,
context.getString(string.video_rating_failed),
Toast.LENGTH_SHORT
).show()
}
})
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}

View File

@ -0,0 +1,63 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.database.Video
import net.schueller.peertube.databinding.RowPlaylistBinding
class PlaylistAdapter(private val mVideos: MutableList<Video>, private val onClick: (Video) -> Unit) : RecyclerView.Adapter<PlaylistAdapter.VideoViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder {
val binding = RowPlaylistBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return VideoViewHolder(binding)
}
override fun onBindViewHolder(holder: VideoViewHolder, position: Int) {
holder.bind(mVideos[position])
}
fun setVideos(videos: List<Video>) {
mVideos.clear()
mVideos.addAll(videos)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return mVideos.size
}
inner class VideoViewHolder(private val binding: RowPlaylistBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(video: Video) {
binding.videoName.text = video.videoName
binding.videoDescription.text = video.videoDescription
binding.root.setOnClickListener { onClick(video) }
}
}
fun getVideoAtPosition(position: Int): Video {
return mVideos[position]
}
}

View File

@ -19,7 +19,9 @@ package net.schueller.peertube.database;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {Server.class}, version = 1)
@Database(entities = {Server.class, Video.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract ServerDao serverDao();
public abstract VideoDao videoDao();
}

View File

@ -0,0 +1,25 @@
package net.schueller.peertube.database
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import kotlinx.parcelize.Parcelize
import java.util.*
@Parcelize
@Entity(tableName = "watch_later")
data class Video(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
@ColumnInfo(name = "video_uuid")
var videoUUID: String,
@ColumnInfo(name = "video_name")
var videoName: String,
@ColumnInfo(name = "video_description")
var videoDescription: String?
) : Parcelable

View File

@ -0,0 +1,39 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface VideoDao {
@Insert
suspend fun insert(video: Video)
@Update
suspend fun update(video: Video)
@Query("DELETE FROM watch_later")
suspend fun deleteAll()
@Delete
suspend fun delete(video: Video)
@get:Query("SELECT * from watch_later ORDER BY video_name DESC")
val allVideos: LiveData<List<Video>>
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import android.app.Application
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class VideoRepository(application: Application) {
private val mVideoDao: VideoDao
val allVideos: LiveData<List<Video>>
get() = mVideoDao.allVideos
init {
val db = VideoRoomDatabase.getDatabase(application)
mVideoDao = db.videoDao()
}
suspend fun update(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.update(video)
}
suspend fun insert(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.insert(video)
}
suspend fun delete(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.delete(video)
}
}

View File

@ -0,0 +1,54 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {Video.class}, version = 1, exportSchema = false)
public abstract class VideoRoomDatabase extends RoomDatabase {
public abstract VideoDao videoDao();
private static volatile VideoRoomDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
public static VideoRoomDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (VideoRoomDatabase.class) {
if (INSTANCE == null) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
VideoRoomDatabase.class, "playlist_database")
.build();
}
}
}
}
return INSTANCE;
}
}

View File

@ -0,0 +1,47 @@
/*
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.database
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class VideoViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: VideoRepository = VideoRepository(application)
val allVideos: LiveData<List<Video>> = mRepository.allVideos
fun insert(video: Video) {
viewModelScope.launch {
mRepository.insert(video)
}
}
fun update(video: Video) {
viewModelScope.launch {
mRepository.update(video)
}
}
fun delete(video: Video) {
viewModelScope.launch {
mRepository.delete(video)
}
}
}

View File

@ -16,54 +16,36 @@
*/
package net.schueller.peertube.fragment
import android.Manifest
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import android.content.res.ColorStateList
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import net.schueller.peertube.R
import net.schueller.peertube.service.VideoPlayerService
import android.app.Activity
import android.content.Context
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.helper.ErrorHelper
import androidx.core.app.ActivityCompat
import android.content.pm.PackageManager
import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import com.squareup.picasso.Picasso
import android.widget.TextView
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.View
import android.widget.Button
import android.widget.ImageView
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.R
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
import net.schueller.peertube.intents.Intents
import net.schueller.peertube.database.VideoViewModel
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.CommentThread
import net.schueller.peertube.model.Rating
import net.schueller.peertube.model.Video
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetUserService
import net.schueller.peertube.network.Session
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.service.VideoPlayerService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.Exception
class VideoMetaDataFragment : Fragment() {
private var videoRating: Rating? = null
@ -73,12 +55,14 @@ class VideoMetaDataFragment : Fragment() {
private lateinit var videoDescriptionFragment: VideoDescriptionFragment
private val mVideoViewModel: VideoViewModel by activityViewModels()
var isLeaveAppExpected = false
private set
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
@ -94,11 +78,11 @@ class VideoMetaDataFragment : Fragment() {
// show full description fragment
videoDescriptionFragment = VideoDescriptionFragment.newInstance(video, this)
childFragmentManager.beginTransaction()
.add(R.id.video_meta_data_fragment, videoDescriptionFragment, VideoDescriptionFragment.TAG).commit()
.add(R.id.video_meta_data_fragment, videoDescriptionFragment, VideoDescriptionFragment.TAG).commit()
}
fun hideDescriptionFragment() {
val fragment: Fragment? = childFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
val fragment: Fragment? = childFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
if (fragment != null) {
childFragmentManager.beginTransaction().remove(fragment).commit()
}
@ -113,10 +97,10 @@ class VideoMetaDataFragment : Fragment() {
val activity: Activity? = activity
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
GetVideoDataService::class.java
)
// related videos
@ -149,8 +133,8 @@ class VideoMetaDataFragment : Fragment() {
videoOptions.setOnClickListener {
val videoOptionsFragment = VideoOptionsFragment.newInstance(mService, video.files)
videoOptionsFragment.show(
getActivity()!!.supportFragmentManager,
VideoOptionsFragment.TAG
getActivity()!!.supportFragmentManager,
VideoOptionsFragment.TAG
)
}
}
@ -166,9 +150,9 @@ class VideoMetaDataFragment : Fragment() {
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call: Call<CommentThread> = service.getCommentThreads(videoId, start, count, sort)
call.enqueue(object : Callback<CommentThread?> {
@ -176,7 +160,7 @@ class VideoMetaDataFragment : Fragment() {
if (response.body() != null) {
val commentThread = response.body()
if (commentThread != null) {
mMultiViewAdapter!!.setVideoComment(commentThread);
mMultiViewAdapter!!.setVideoComment(commentThread)
}
}
}
@ -197,8 +181,8 @@ class VideoMetaDataFragment : Fragment() {
val filter: String? = null
val sharedPref = context?.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
var nsfw = "false"
@ -211,9 +195,9 @@ class VideoMetaDataFragment : Fragment() {
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call: Call<VideoList> = service.getVideosData(start, count, sort, nsfw, filter, languages)
/*Log the URL called*/Log.d("URL Called", call.request().url.toString() + "")
@ -234,6 +218,12 @@ class VideoMetaDataFragment : Fragment() {
}
})
}
fun saveToPlaylist(video: Video) {
val playlistVideo: net.schueller.peertube.database.Video = net.schueller.peertube.database.Video(videoUUID = video.uuid, videoName = video.name, videoDescription = video.description)
mVideoViewModel.insert(playlistVideo)
}
companion object {
const val TAG = "VMDF"
}

View File

@ -1,175 +1,188 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
tools:context=".activity.MeActivity"
android:orientation="vertical">
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.MeActivity"
android:orientation="vertical">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar_me"
android:id="@+id/appbar_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp" />
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar_me"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:elevation="4dp"/>
</com.google.android.material.appbar.AppBarLayout>
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:id="@+id/a_me_account_line"
android:orientation="horizontal">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal">
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/a_me_avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="0dp"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"/>
<LinearLayout
android:visibility="gone"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:id="@+id/a_me_account_line"
android:orientation="horizontal">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/a_me_username"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
android:orientation="horizontal">
<TextView
android:id="@+id/a_me_email"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
<de.hdodenhof.circleimageview.CircleImageView
android:id="@+id/a_me_avatar"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_marginTop="0dp"
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:layout_marginLeft="12dp"
android:layout_marginRight="12dp"/>
<TextView
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_logout_button"
android:id="@+id/a_me_logout"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical">
<TextView
android:id="@+id/a_me_username"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
<TextView
android:id="@+id/a_me_email"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text=""
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
<TextView
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_logout_button"
android:id="@+id/a_me_logout"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/darker_gray"/>
<LinearLayout
android:id="@+id/a_me_playlist"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_settings_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/playlist"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
<LinearLayout
android:id="@+id/a_me_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_settings_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_activity_settings"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
<LinearLayout
android:id="@+id/a_me_helpnfeedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_help_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_help_and_feedback_button"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"/>
</LinearLayout>
</LinearLayout>
<View
android:layout_width="match_parent"
android:layout_height="1dp"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:background="@android:color/darker_gray" />
</ScrollView>
<LinearLayout
android:id="@+id/a_me_settings"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<!-- <LinearLayout-->
<!-- android:layout_marginBottom="0dp"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="vertical">-->
<TextView
android:drawableStart="@drawable/ic_baseline_settings_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/title_activity_settings"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
</LinearLayout>
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_username"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<LinearLayout
android:id="@+id/a_me_helpnfeedback"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="12dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:layout_marginBottom="12dp"
android:orientation="horizontal">
<TextView
android:drawableStart="@drawable/ic_baseline_help_24"
android:drawablePadding="16dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/me_help_and_feedback_button"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
</LinearLayout>
</LinearLayout>
</ScrollView>
<!-- <LinearLayout-->
<!-- android:layout_marginBottom="0dp"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:orientation="vertical">-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_username"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_email"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<!-- </LinearLayout>-->
<!-- <androidx.appcompat.widget.AppCompatTextView-->
<!-- android:id="@+id/account_email"-->
<!-- android:layout_width="match_parent"-->
<!-- android:layout_height="wrap_content"-->
<!-- android:text="" />-->
<!-- </LinearLayout>-->
</LinearLayout>

View File

@ -0,0 +1,44 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".activity.ServerAddressBookActivity"
android:id="@+id/server_book">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appbar_server_address_book"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.appcompat.widget.Toolbar
android:id="@+id/tool_bar_server_address_book"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_scrollFlags="scroll|enterAlways"
android:elevation="4dp"/>
</com.google.android.material.appbar.AppBarLayout>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="@string/appbar_scrolling_view_behavior"
>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/server_list_recyclerview"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/darker_gray"
tools:listitem="@layout/row_playlist"
app:layoutManager="androidx.recyclerview.widget.LinearLayoutManager"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View File

@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.cardview.widget.CardView
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:card_view="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clickable="true"
android:focusable="true"
card_view:cardCornerRadius="0dp"
card_view:cardElevation="0dp"
card_view:cardUseCompatPadding="true">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_height="wrap_content"
android:layout_width="match_parent"
android:padding="12dp">
<TextView
android:id="@+id/video_name"
android:layout_width="match_parent"
android:layout_height="wrap_content"
card_view:layout_constraintTop_toTopOf="parent"
card_view:layout_constraintStart_toStartOf="parent"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title"
tools:text="@tools:sample/lorem"
/>
<TextView
android:id="@+id/video_description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:maxLines="2"
android:ellipsize="end"
card_view:layout_constraintTop_toBottomOf="@id/video_name"
card_view:layout_constraintStart_toStartOf="parent"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead"
tools:text="@tools:sample/lorem"
/>
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.cardview.widget.CardView>

View File

@ -384,4 +384,8 @@
<string name="video_owner_fqdn_line">%1$s@%2$s</string>
<string name="video_sub_del_alert_title">Unsubscribe</string>
<string name="video_sub_del_alert_msg">Are you sure you would like to unsubscribe?</string>
<string name="saved_to_playlist">Saved to playlist</string>
<string name="remove_video">Remove Video</string>
<string name="remove_video_warning_message">Are you sure you want to remove this video from playlist?</string>
<string name="playlist">Playlist</string>
</resources>