Player update
This commit is contained in:
parent
0d720105ce
commit
b83130125d
|
@ -8,7 +8,7 @@ ENV ANDROID_SDK_CHECKSUM 124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e
|
|||
# higher version casues Warning: Failed to find package
|
||||
ENV ANDROID_BUILD_TOOLS_VERSION 30.0.2
|
||||
ENV ANDROID_SDK_ROOT /usr/local/android-sdk-linux
|
||||
ENV ANDROID_VERSION 30
|
||||
ENV ANDROID_VERSION 32
|
||||
# ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
|
||||
ENV PATH ${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin
|
||||
|
||||
|
|
|
@ -39,13 +39,13 @@ else {
|
|||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.schueller.peertube"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 32
|
||||
versionCode 1069
|
||||
versionName "1.8.3"
|
||||
buildConfigField "long", "BUILD_TIME", readPropertyWithDefault('buildTimestamp', System.currentTimeMillis()) + 'L'
|
||||
|
@ -94,10 +94,10 @@ android {
|
|||
|
||||
}
|
||||
|
||||
def room_version = "2.3.0"
|
||||
def lifecycleVersion = '2.3.1'
|
||||
def exoplayer = '2.12.3'
|
||||
def fragment_version = "1.3.6"
|
||||
def room_version = "2.4.0"
|
||||
def lifecycleVersion = '2.4.0'
|
||||
def exoplayer = '2.16.1'
|
||||
def fragment_version = "1.4.0"
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
@ -105,8 +105,8 @@ dependencies {
|
|||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// Layouts and design
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
|
@ -118,7 +118,7 @@ dependencies {
|
|||
implementation 'com.mikepenz:fontawesome-typeface:5.9.0.2-kotlin@aar'
|
||||
|
||||
// http client / REST
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
|
||||
// image downloading and caching library
|
||||
|
|
|
@ -41,7 +41,7 @@ import java.util.*
|
|||
|
||||
class ServerAddressBookActivity : CommonActivity() {
|
||||
|
||||
private val TAG = "ServerAddressBookActivity"
|
||||
private val TAG = "ServerAddBookAct"
|
||||
|
||||
private val mServerViewModel: ServerViewModel by viewModels()
|
||||
private var addServerFragment: AddServerFragment? = null
|
||||
|
@ -133,15 +133,15 @@ class ServerAddressBookActivity : CommonActivity() {
|
|||
AlertDialog.Builder(this@ServerAddressBookActivity)
|
||||
.setTitle(getString(R.string.server_book_del_alert_title))
|
||||
.setMessage(getString(R.string.server_book_del_alert_msg))
|
||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
val position = viewHolder.adapterPosition
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
val server = adapter.getServerAtPosition(position)
|
||||
// Toast.makeText(ServerAddressBookActivity.this, "Deleting " +
|
||||
// server.getServerName(), Toast.LENGTH_LONG).show();
|
||||
// Delete the server
|
||||
mServerViewModel.delete(server)
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.adapterPosition) }
|
||||
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.show()
|
||||
}
|
||||
|
|
|
@ -19,6 +19,7 @@ package net.schueller.peertube.activity
|
|||
import android.Manifest.permission
|
||||
import android.R.drawable
|
||||
import android.R.string
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog.Builder
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
|
@ -33,6 +34,7 @@ import android.view.MenuItem
|
|||
import android.view.MenuItem.OnActionExpandListener
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SearchView.OnSuggestionListener
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
|
@ -116,7 +118,7 @@ class VideoListActivity : CommonActivity() {
|
|||
Builder(this@VideoListActivity)
|
||||
.setTitle(getString(R.string.clear_search_history))
|
||||
.setMessage(getString(R.string.clear_search_history_prompt))
|
||||
.setPositiveButton(string.yes) { _, _ ->
|
||||
.setPositiveButton(string.ok) { _, _ ->
|
||||
val suggestions = SearchRecentSuggestions(
|
||||
applicationContext,
|
||||
SearchSuggestionsProvider.AUTHORITY,
|
||||
|
@ -124,7 +126,7 @@ class VideoListActivity : CommonActivity() {
|
|||
)
|
||||
suggestions.clearHistory()
|
||||
}
|
||||
.setNegativeButton(string.no, null)
|
||||
.setNegativeButton(string.cancel, null)
|
||||
.setIcon(drawable.ic_dialog_alert)
|
||||
.show()
|
||||
true
|
||||
|
@ -160,8 +162,7 @@ class VideoListActivity : CommonActivity() {
|
|||
position
|
||||
) as Cursor
|
||||
return cursor.getString(
|
||||
cursor
|
||||
.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
|
||||
cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -178,15 +179,26 @@ class VideoListActivity : CommonActivity() {
|
|||
stopService(Intent(this, VideoPlayerService::class.java))
|
||||
}
|
||||
|
||||
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == SWITCH_INSTANCE) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
// public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// super.onActivityResult(requestCode, resultCode, data)
|
||||
// if (requestCode == SWITCH_INSTANCE) {
|
||||
// if (resultCode == RESULT_OK) {
|
||||
// loadVideos(currentStart, count, sort, filter)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private var resultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openActivityForResult(intent: Intent) {
|
||||
resultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
|
@ -213,7 +225,7 @@ class VideoListActivity : CommonActivity() {
|
|||
}
|
||||
id.action_server_address_book -> {
|
||||
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
|
||||
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
|
||||
openActivityForResult(addressBookActivityIntent)
|
||||
return false
|
||||
}
|
||||
else -> {
|
||||
|
@ -461,7 +473,7 @@ class VideoListActivity : CommonActivity() {
|
|||
// new IconicsDrawable(this, FontAwesome.Icon.faw_user_circle));
|
||||
|
||||
// Click Listener
|
||||
navigation.setOnNavigationItemSelectedListener { menuItem: MenuItem ->
|
||||
navigation.setOnItemSelectedListener { menuItem: MenuItem ->
|
||||
when (menuItem.itemId) {
|
||||
id.navigation_overview -> {
|
||||
// TODO
|
||||
|
@ -470,7 +482,7 @@ class VideoListActivity : CommonActivity() {
|
|||
loadOverview(currentPage)
|
||||
overViewActive = true
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_trending -> {
|
||||
//Log.v(TAG, "navigation_trending");
|
||||
|
@ -482,7 +494,7 @@ class VideoListActivity : CommonActivity() {
|
|||
subscriptions = false
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_recent -> {
|
||||
if (!isLoading) {
|
||||
|
@ -493,7 +505,7 @@ class VideoListActivity : CommonActivity() {
|
|||
subscriptions = false
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_local -> {
|
||||
//Log.v(TAG, "navigation_trending");
|
||||
|
@ -505,15 +517,15 @@ class VideoListActivity : CommonActivity() {
|
|||
subscriptions = false
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_subscriptions -> //Log.v(TAG, "navigation_subscriptions");
|
||||
if (!Session.getInstance().isLoggedIn) {
|
||||
// Intent intent = new Intent(this, LoginActivity.class);
|
||||
// this.startActivity(intent);
|
||||
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
|
||||
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
|
||||
return@setOnNavigationItemSelectedListener false
|
||||
openActivityForResult(addressBookActivityIntent)
|
||||
return@setOnItemSelectedListener false
|
||||
} else {
|
||||
if (!isLoading) {
|
||||
overViewActive = false
|
||||
|
@ -523,7 +535,7 @@ class VideoListActivity : CommonActivity() {
|
|||
subscriptions = true
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
|
@ -574,6 +586,5 @@ class VideoListActivity : CommonActivity() {
|
|||
|
||||
const val EXTRA_VIDEOID = "VIDEOID"
|
||||
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
|
||||
const val SWITCH_INSTANCE = 2
|
||||
}
|
||||
}
|
|
@ -1,502 +0,0 @@
|
|||
/*
|
||||
* 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.annotation.SuppressLint;
|
||||
import android.app.AppOpsManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.app.RemoteAction;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment;
|
||||
import net.schueller.peertube.fragment.VideoPlayerFragment;
|
||||
import net.schueller.peertube.service.VideoPlayerService;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PAUSE;
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PLAY;
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
|
||||
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
|
||||
|
||||
public class VideoPlayActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "VideoPlayActivity";
|
||||
|
||||
static boolean floatMode = false;
|
||||
|
||||
private static final int REQUEST_CODE = 101;
|
||||
private BroadcastReceiver receiver;
|
||||
|
||||
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
|
||||
@SuppressLint("NewApi")
|
||||
public void makePipControls() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
ArrayList<RemoteAction> actions = new ArrayList<>();
|
||||
|
||||
Intent actionIntent = new Intent(getString(R.string.app_background_audio));
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
@SuppressLint({"NewApi", "LocalSuppress"}) Icon icon = Icon.createWithResource(getApplicationContext(), android.R.drawable.stat_sys_speakerphone);
|
||||
@SuppressLint({"NewApi", "LocalSuppress"}) RemoteAction remoteAction = new RemoteAction(icon, "close pip", "from pip window custom command", pendingIntent);
|
||||
actions.add(remoteAction);
|
||||
|
||||
actionIntent = new Intent(ACTION_STOP);
|
||||
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop);
|
||||
remoteAction = new RemoteAction(icon, "play", "stop the media", pendingIntent);
|
||||
actions.add(remoteAction);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
if (videoPlayerFragment.isPaused()) {
|
||||
Log.e(TAG, "setting actions with play button");
|
||||
actionIntent = new Intent(ACTION_PLAY);
|
||||
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_play);
|
||||
remoteAction = new RemoteAction(icon, "play", "play the media", pendingIntent);
|
||||
} else {
|
||||
Log.e(TAG, "setting actions with pause button");
|
||||
actionIntent = new Intent(ACTION_PAUSE);
|
||||
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause);
|
||||
remoteAction = new RemoteAction(icon, "pause", "pause the media", pendingIntent);
|
||||
}
|
||||
actions.add(remoteAction);
|
||||
|
||||
|
||||
//add custom actions to pip window
|
||||
PictureInPictureParams params =
|
||||
new PictureInPictureParams.Builder()
|
||||
.setActions(actions)
|
||||
.build();
|
||||
setPictureInPictureParams(params);
|
||||
}
|
||||
|
||||
public void changedToPipMode() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.showControls(false);
|
||||
//create custom actions
|
||||
makePipControls();
|
||||
|
||||
//setup receiver to handle customer actions
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STOP);
|
||||
filter.addAction(ACTION_PAUSE);
|
||||
filter.addAction(ACTION_PLAY);
|
||||
filter.addAction((getString(R.string.app_background_audio)));
|
||||
receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
assert action != null;
|
||||
if (action.equals(ACTION_PAUSE)) {
|
||||
videoPlayerFragment.pauseVideo();
|
||||
makePipControls();
|
||||
}
|
||||
if (action.equals(ACTION_PLAY)) {
|
||||
videoPlayerFragment.unPauseVideo();
|
||||
makePipControls();
|
||||
}
|
||||
|
||||
if (action.equals(getString(R.string.app_background_audio))) {
|
||||
unregisterReceiver(receiver);
|
||||
finish();
|
||||
}
|
||||
if (action.equals(ACTION_STOP)) {
|
||||
unregisterReceiver(receiver);
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(receiver, filter);
|
||||
|
||||
Log.v(TAG, "switched to pip ");
|
||||
floatMode = true;
|
||||
videoPlayerFragment.showControls(false);
|
||||
}
|
||||
|
||||
public void changedToNormalMode() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.showControls(true);
|
||||
if (receiver != null) {
|
||||
unregisterReceiver(receiver);
|
||||
}
|
||||
Log.v(TAG, "switched to normal");
|
||||
floatMode = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Set theme
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
setTheme(getResources().getIdentifier(
|
||||
sharedPref.getString(
|
||||
getString(R.string.pref_theme_key),
|
||||
getString(R.string.app_default_theme)
|
||||
),
|
||||
"style",
|
||||
getPackageName())
|
||||
);
|
||||
|
||||
setContentView(R.layout.activity_video_play);
|
||||
|
||||
// get video ID
|
||||
Intent intent = getIntent();
|
||||
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
String playingVideo = videoPlayerFragment.getVideoUuid();
|
||||
Log.v(TAG, "oncreate click: " + videoUuid + " is trying to replace: " + playingVideo);
|
||||
|
||||
if (TextUtils.isEmpty(playingVideo)) {
|
||||
Log.v(TAG, "oncreate no video currently playing");
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else if (!playingVideo.equals(videoUuid)) {
|
||||
Log.v(TAG, "oncreate different video playing currently");
|
||||
videoPlayerFragment.stopVideo();
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else {
|
||||
Log.v(TAG, "oncreate same video playing currently");
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
int orientation = this.getResources().getConfiguration().orientation;
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
assert videoPlayerFragment != null;
|
||||
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
|
||||
Log.v(TAG, "new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.getVideoUuid());
|
||||
String playingVideo = videoPlayerFragment.getVideoUuid();
|
||||
|
||||
if (TextUtils.isEmpty(playingVideo)) {
|
||||
Log.v(TAG, "new intent no video currently playing");
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else if (!playingVideo.equals(videoUuid)) {
|
||||
Log.v(TAG, "new intent different video playing currently");
|
||||
videoPlayerFragment.stopVideo();
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else {
|
||||
Log.v(TAG, "new intent same video playing currently");
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
int orientation = this.getResources().getConfiguration().orientation;
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
Log.v(TAG, "onConfigurationChanged()...");
|
||||
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Checking the orientation changes of the screen
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true);
|
||||
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
setOrientation(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setOrientation(Boolean isLandscape) {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
VideoMetaDataFragment videoMetaFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) videoPlayerFragment.requireView().getLayoutParams();
|
||||
params.width = FrameLayout.LayoutParams.MATCH_PARENT;
|
||||
params.height = isLandscape ? FrameLayout.LayoutParams.MATCH_PARENT : (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 250, getResources().getDisplayMetrics());
|
||||
|
||||
videoPlayerFragment.requireView().setLayoutParams(params);
|
||||
|
||||
if (videoMetaFragment != null) {
|
||||
FragmentTransaction transaction = fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
if (isLandscape) {
|
||||
transaction.hide(videoMetaFragment);
|
||||
} else {
|
||||
transaction.show(videoMetaFragment);
|
||||
}
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
videoPlayerFragment.setIsFullscreen(isLandscape);
|
||||
|
||||
if ( isLandscape ) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.destroyVideo();
|
||||
|
||||
super.onDestroy();
|
||||
Log.v(TAG, "onDestroy...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
Log.v(TAG, "onPause()...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
Log.v(TAG, "onResume()...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.stopVideo();
|
||||
|
||||
Log.v(TAG, "onStop()...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
Log.v(TAG, "onStart()...");
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void onUserLeaveHint() {
|
||||
|
||||
Log.v(TAG, "onUserLeaveHint()...");
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
|
||||
|
||||
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
assert backgroundBehavior != null;
|
||||
if ( videoMetaDataFragment.isLeaveAppExpected() )
|
||||
{
|
||||
super.onUserLeaveHint();
|
||||
return;
|
||||
}
|
||||
|
||||
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
|
||||
Log.v(TAG, "stop the video");
|
||||
|
||||
videoPlayerFragment.pauseVideo();
|
||||
stopService(new Intent(this, VideoPlayerService.class));
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
|
||||
Log.v(TAG, "play the Audio");
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
|
||||
Log.v(TAG, "play in floating video");
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip");
|
||||
enterPipMode();
|
||||
} else {
|
||||
Log.v(TAG, "unable to use pip");
|
||||
}
|
||||
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback");
|
||||
super.onBackPressed();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// @RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@SuppressLint("NewApi")
|
||||
public void onBackPressed() {
|
||||
|
||||
Log.v(TAG, "onBackPressed()...");
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
|
||||
// copying Youtube behavior to have back button exit full screen.
|
||||
if (videoPlayerFragment.getIsFullscreen()) {
|
||||
Log.v(TAG, "exiting full screen");
|
||||
videoPlayerFragment.fullScreenToggle();
|
||||
return;
|
||||
}
|
||||
// pause video if pref is enabled
|
||||
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
|
||||
videoPlayerFragment.pauseVideo();
|
||||
}
|
||||
|
||||
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
|
||||
|
||||
assert backgroundBehavior != null;
|
||||
|
||||
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
|
||||
Log.v(TAG, "stop the video");
|
||||
videoPlayerFragment.pauseVideo();
|
||||
stopService(new Intent(this, VideoPlayerService.class));
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
|
||||
Log.v(TAG, "play the Audio");
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
|
||||
Log.v(TAG, "play in floating video");
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip");
|
||||
enterPipMode();
|
||||
//fixes problem where back press doesn't bring up video list after returning from PIP mode
|
||||
Intent intentSettings = new Intent(this, VideoListActivity.class);
|
||||
this.startActivity(intentSettings);
|
||||
} else {
|
||||
Log.v(TAG, "Unable to enter PIP mode");
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback");
|
||||
super.onBackPressed();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public void enterPipMode() {
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
final VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById( R.id.video_player_fragment );
|
||||
|
||||
if ( videoPlayerFragment.getVideoAspectRatio() == 0 ) {
|
||||
Log.i( TAG, "impossible to switch to pip" );
|
||||
} else {
|
||||
Rational rational = new Rational( (int) ( videoPlayerFragment.getVideoAspectRatio() * 100 ), 100 );
|
||||
PictureInPictureParams mParams =
|
||||
new PictureInPictureParams.Builder()
|
||||
.setAspectRatio( rational )
|
||||
// .setSourceRectHint(new Rect(0,500,400,600))
|
||||
.build();
|
||||
|
||||
enterPictureInPictureMode( mParams );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
if (videoPlayerFragment != null) {
|
||||
|
||||
if (isInPictureInPictureMode) {
|
||||
changedToPipMode();
|
||||
Log.v(TAG, "switched to pip ");
|
||||
videoPlayerFragment.useController(false);
|
||||
} else {
|
||||
changedToNormalMode();
|
||||
Log.v(TAG, "switched to normal");
|
||||
videoPlayerFragment.useController(true);
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "videoPlayerFragment is NULL");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,461 @@
|
|||
/*
|
||||
* 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 androidx.appcompat.app.AppCompatActivity
|
||||
import android.annotation.SuppressLint
|
||||
import net.schueller.peertube.fragment.VideoPlayerFragment
|
||||
import net.schueller.peertube.R
|
||||
import android.app.RemoteAction
|
||||
import android.app.PendingIntent
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.*
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.FrameLayout
|
||||
import android.util.TypedValue
|
||||
import android.view.WindowManager
|
||||
import net.schueller.peertube.service.VideoPlayerService
|
||||
import net.schueller.peertube.helper.VideoHelper
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.schueller.peertube.fragment.VideoDescriptionFragment
|
||||
import java.util.ArrayList
|
||||
|
||||
class VideoPlayActivity : CommonActivity() {
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
|
||||
@SuppressLint("NewApi")
|
||||
fun makePipControls() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
val actions = ArrayList<RemoteAction>()
|
||||
var actionIntent = Intent(getString(R.string.app_background_audio))
|
||||
var pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
@SuppressLint("NewApi", "LocalSuppress") var icon = Icon.createWithResource(
|
||||
applicationContext, android.R.drawable.stat_sys_speakerphone
|
||||
)
|
||||
@SuppressLint("NewApi", "LocalSuppress") var remoteAction =
|
||||
RemoteAction(icon!!, "close pip", "from pip window custom command", pendingIntent!!)
|
||||
actions.add(remoteAction)
|
||||
actionIntent = Intent(PlayerNotificationManager.ACTION_STOP)
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
icon = Icon.createWithResource(
|
||||
applicationContext,
|
||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop
|
||||
)
|
||||
remoteAction = RemoteAction(icon, "play", "stop the media", pendingIntent)
|
||||
actions.add(remoteAction)
|
||||
assert(videoPlayerFragment != null)
|
||||
if (videoPlayerFragment!!.isPaused) {
|
||||
Log.e(TAG, "setting actions with play button")
|
||||
actionIntent = Intent(PlayerNotificationManager.ACTION_PLAY)
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
icon = Icon.createWithResource(
|
||||
applicationContext,
|
||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play
|
||||
)
|
||||
remoteAction = RemoteAction(icon, "play", "play the media", pendingIntent)
|
||||
} else {
|
||||
Log.e(TAG, "setting actions with pause button")
|
||||
actionIntent = Intent(PlayerNotificationManager.ACTION_PAUSE)
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
icon = Icon.createWithResource(
|
||||
applicationContext,
|
||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause
|
||||
)
|
||||
remoteAction = RemoteAction(icon, "pause", "pause the media", pendingIntent)
|
||||
}
|
||||
actions.add(remoteAction)
|
||||
|
||||
|
||||
//add custom actions to pip window
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setActions(actions)
|
||||
.build()
|
||||
setPictureInPictureParams(params)
|
||||
}
|
||||
|
||||
private fun changedToPipMode() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.showControls(false)
|
||||
//create custom actions
|
||||
makePipControls()
|
||||
|
||||
//setup receiver to handle customer actions
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(PlayerNotificationManager.ACTION_STOP)
|
||||
filter.addAction(PlayerNotificationManager.ACTION_PAUSE)
|
||||
filter.addAction(PlayerNotificationManager.ACTION_PLAY)
|
||||
filter.addAction(getString(R.string.app_background_audio))
|
||||
receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action!!
|
||||
if (action == PlayerNotificationManager.ACTION_PAUSE) {
|
||||
videoPlayerFragment.pauseVideo()
|
||||
makePipControls()
|
||||
}
|
||||
if (action == PlayerNotificationManager.ACTION_PLAY) {
|
||||
videoPlayerFragment.unPauseVideo()
|
||||
makePipControls()
|
||||
}
|
||||
if (action == getString(R.string.app_background_audio)) {
|
||||
unregisterReceiver(receiver)
|
||||
finish()
|
||||
}
|
||||
if (action == PlayerNotificationManager.ACTION_STOP) {
|
||||
unregisterReceiver(receiver)
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
registerReceiver(receiver, filter)
|
||||
Log.v(TAG, "switched to pip ")
|
||||
floatMode = true
|
||||
videoPlayerFragment.showControls(false)
|
||||
}
|
||||
|
||||
private fun changedToNormalMode() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.showControls(true)
|
||||
if (receiver != null) {
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
Log.v(TAG, "switched to normal")
|
||||
floatMode = false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Set theme
|
||||
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
setTheme(
|
||||
resources.getIdentifier(
|
||||
sharedPref.getString(
|
||||
getString(R.string.pref_theme_key),
|
||||
getString(R.string.app_default_theme)
|
||||
),
|
||||
"style",
|
||||
packageName
|
||||
)
|
||||
)
|
||||
setContentView(R.layout.activity_video_play)
|
||||
|
||||
// get video ID
|
||||
val intent = intent
|
||||
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
val playingVideo = videoPlayerFragment.videoUuid
|
||||
Log.v(TAG, "oncreate click: $videoUuid is trying to replace: $playingVideo")
|
||||
when {
|
||||
TextUtils.isEmpty(playingVideo) -> {
|
||||
Log.v(TAG, "oncreate no video currently playing")
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
playingVideo != videoUuid -> {
|
||||
Log.v(TAG, "oncreate different video playing currently")
|
||||
videoPlayerFragment.stopVideo()
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
else -> {
|
||||
Log.v(TAG, "oncreate same video playing currently")
|
||||
}
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
val orientation = this.resources.configuration.orientation
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
|
||||
Log.v(
|
||||
TAG,
|
||||
"new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.videoUuid
|
||||
)
|
||||
val playingVideo = videoPlayerFragment.videoUuid
|
||||
when {
|
||||
TextUtils.isEmpty(playingVideo) -> {
|
||||
Log.v(TAG, "new intent no video currently playing")
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
playingVideo != videoUuid -> {
|
||||
Log.v(TAG, "new intent different video playing currently")
|
||||
videoPlayerFragment.stopVideo()
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
else -> {
|
||||
Log.v(TAG, "new intent same video playing currently")
|
||||
}
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
val orientation = this.resources.configuration.orientation
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
Log.v(TAG, "onConfigurationChanged()...")
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
// Checking the orientation changes of the screen
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true)
|
||||
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
setOrientation(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOrientation(isLandscape: Boolean) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
val videoMetaFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
|
||||
assert(videoPlayerFragment != null)
|
||||
val params = videoPlayerFragment!!.requireView().layoutParams as RelativeLayout.LayoutParams
|
||||
params.width = FrameLayout.LayoutParams.MATCH_PARENT
|
||||
params.height =
|
||||
if (isLandscape) FrameLayout.LayoutParams.MATCH_PARENT else TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
250f,
|
||||
resources.displayMetrics
|
||||
)
|
||||
.toInt()
|
||||
videoPlayerFragment.requireView().layoutParams = params
|
||||
if (videoMetaFragment != null) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
if (isLandscape) {
|
||||
transaction.hide(videoMetaFragment)
|
||||
} else {
|
||||
transaction.show(videoMetaFragment)
|
||||
}
|
||||
transaction.commit()
|
||||
}
|
||||
videoPlayerFragment.setIsFullscreen(isLandscape)
|
||||
if (isLandscape) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.destroyVideo()
|
||||
super.onDestroy()
|
||||
Log.v(TAG, "onDestroy...")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.v(TAG, "onPause()...")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.v(TAG, "onResume()...")
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.stopVideo()
|
||||
|
||||
// TODO: doesn't remove fragment??
|
||||
val fragment: Fragment? = supportFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
|
||||
if (fragment != null) {
|
||||
Log.v(TAG, "remove VideoDescriptionFragment")
|
||||
supportFragmentManager.beginTransaction().remove(fragment).commit()
|
||||
}
|
||||
|
||||
Log.v(TAG, "onStop()...")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Log.v(TAG, "onStart()...")
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public override fun onUserLeaveHint() {
|
||||
Log.v(TAG, "onUserLeaveHint()...")
|
||||
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
val videoMetaDataFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
|
||||
val backgroundBehavior = sharedPref.getString(
|
||||
getString(R.string.pref_background_behavior_key),
|
||||
getString(R.string.pref_background_stop_key)
|
||||
)
|
||||
assert(videoPlayerFragment != null)
|
||||
assert(backgroundBehavior != null)
|
||||
if (videoMetaDataFragment!!.isLeaveAppExpected) {
|
||||
super.onUserLeaveHint()
|
||||
return
|
||||
}
|
||||
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
|
||||
Log.v(TAG, "stop the video")
|
||||
videoPlayerFragment!!.pauseVideo()
|
||||
stopService(Intent(this, VideoPlayerService::class.java))
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
|
||||
Log.v(TAG, "play the Audio")
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
|
||||
Log.v(TAG, "play in floating video")
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (VideoHelper.canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip")
|
||||
enterPipMode()
|
||||
} else {
|
||||
Log.v(TAG, "unable to use pip")
|
||||
}
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback")
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
// @RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@SuppressLint("NewApi")
|
||||
override fun onBackPressed() {
|
||||
Log.v(TAG, "onBackPressed()...")
|
||||
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
|
||||
// copying Youtube behavior to have back button exit full screen.
|
||||
if (videoPlayerFragment.getIsFullscreen()) {
|
||||
Log.v(TAG, "exiting full screen")
|
||||
videoPlayerFragment.fullScreenToggle()
|
||||
return
|
||||
}
|
||||
// pause video if pref is enabled
|
||||
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
|
||||
videoPlayerFragment.pauseVideo()
|
||||
}
|
||||
val backgroundBehavior = sharedPref.getString(
|
||||
getString(R.string.pref_background_behavior_key),
|
||||
getString(R.string.pref_background_stop_key)
|
||||
)!!
|
||||
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
|
||||
Log.v(TAG, "stop the video")
|
||||
videoPlayerFragment.pauseVideo()
|
||||
stopService(Intent(this, VideoPlayerService::class.java))
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
|
||||
Log.v(TAG, "play the Audio")
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
|
||||
Log.v(TAG, "play in floating video")
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (VideoHelper.canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip")
|
||||
enterPipMode()
|
||||
//fixes problem where back press doesn't bring up video list after returning from PIP mode
|
||||
val intentSettings = Intent(this, VideoListActivity::class.java)
|
||||
this.startActivity(intentSettings)
|
||||
} else {
|
||||
Log.v(TAG, "Unable to enter PIP mode")
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback")
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun enterPipMode() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
if (videoPlayerFragment!!.videoAspectRatio == 0.toFloat()) {
|
||||
Log.i(TAG, "impossible to switch to pip")
|
||||
} else {
|
||||
val rational = Rational((videoPlayerFragment.videoAspectRatio * 100).toInt(), 100)
|
||||
val mParams = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(rational) // .setSourceRectHint(new Rect(0,500,400,600))
|
||||
.build()
|
||||
enterPictureInPictureMode(mParams)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration
|
||||
) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
if (videoPlayerFragment != null) {
|
||||
if (isInPictureInPictureMode) {
|
||||
changedToPipMode()
|
||||
Log.v(TAG, "switched to pip ")
|
||||
videoPlayerFragment.useController(false)
|
||||
} else {
|
||||
changedToNormalMode()
|
||||
Log.v(TAG, "switched to normal")
|
||||
videoPlayerFragment.useController(true)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "videoPlayerFragment is NULL")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VideoPlayActivity"
|
||||
var floatMode = false
|
||||
private const val REQUEST_CODE = 101
|
||||
}
|
||||
}
|
|
@ -4,19 +4,14 @@ import android.view.LayoutInflater
|
|||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
|
||||
import net.schueller.peertube.databinding.ItemChannelTitleBinding
|
||||
import net.schueller.peertube.databinding.ItemTagTitleBinding
|
||||
import net.schueller.peertube.databinding.RowVideoListBinding
|
||||
import net.schueller.peertube.model.Category
|
||||
import net.schueller.peertube.model.Channel
|
||||
import net.schueller.peertube.model.TagVideo
|
||||
import net.schueller.peertube.model.Video
|
||||
import net.schueller.peertube.model.VideoList
|
||||
import net.schueller.peertube.databinding.*
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment
|
||||
import net.schueller.peertube.model.*
|
||||
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
|
||||
import net.schueller.peertube.model.ui.VideoMetaViewItem
|
||||
import java.util.ArrayList
|
||||
|
||||
class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
|
||||
class MultiViewRecycleViewAdapter(private val videoMetaDataFragment: VideoMetaDataFragment? = null) : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
|
||||
|
||||
private var items = ArrayList<OverviewRecycleViewItem>()
|
||||
set(value) {
|
||||
|
@ -34,6 +29,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
|||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setVideoMeta(videoMetaViewItem: VideoMetaViewItem) {
|
||||
items.add(videoMetaViewItem)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setCategoryTitle(category: Category) {
|
||||
items.add(category)
|
||||
notifyDataSetChanged()
|
||||
|
@ -49,6 +49,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
|||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setVideoComment(commentThread: CommentThread) {
|
||||
items.add(commentThread)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
items.clear()
|
||||
notifyDataSetChanged()
|
||||
|
@ -83,6 +88,21 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
|||
false
|
||||
)
|
||||
)
|
||||
R.layout.item_video_meta -> MultiViewRecyclerViewHolder.VideoMetaViewHolder(
|
||||
ItemVideoMetaBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
videoMetaDataFragment
|
||||
)
|
||||
R.layout.item_video_comments_overview -> MultiViewRecyclerViewHolder.VideoCommentsViewHolder(
|
||||
ItemVideoCommentsOverviewBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
else -> throw IllegalArgumentException("Invalid ViewType Provided")
|
||||
}
|
||||
}
|
||||
|
@ -93,6 +113,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
|||
is MultiViewRecyclerViewHolder.CategoryViewHolder -> holder.bind(items[position] as Category)
|
||||
is MultiViewRecyclerViewHolder.ChannelViewHolder -> holder.bind(items[position] as Channel)
|
||||
is MultiViewRecyclerViewHolder.TagViewHolder -> holder.bind(items[position] as TagVideo)
|
||||
is MultiViewRecyclerViewHolder.VideoMetaViewHolder -> holder.bind(items[position] as VideoMetaViewItem)
|
||||
is MultiViewRecyclerViewHolder.VideoCommentsViewHolder -> holder.bind(items[position] as CommentThread)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -104,6 +126,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
|||
is Channel -> R.layout.item_channel_title
|
||||
is Category -> R.layout.item_category_title
|
||||
is TagVideo -> R.layout.item_tag_title
|
||||
is VideoMetaViewItem -> R.layout.item_video_meta
|
||||
is CommentThread -> R.layout.item_video_comments_overview
|
||||
else -> { return 0}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -16,47 +16,286 @@
|
|||
*/
|
||||
package net.schueller.peertube.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.gson.JsonObject
|
||||
import com.squareup.picasso.Picasso
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.R.color
|
||||
import net.schueller.peertube.R.string
|
||||
import net.schueller.peertube.activity.AccountActivity
|
||||
import net.schueller.peertube.activity.VideoListActivity
|
||||
import net.schueller.peertube.activity.VideoListActivity.Companion
|
||||
import net.schueller.peertube.activity.VideoPlayActivity
|
||||
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
|
||||
import net.schueller.peertube.databinding.ItemChannelTitleBinding
|
||||
import net.schueller.peertube.databinding.RowVideoListBinding
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getDuration
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
|
||||
import net.schueller.peertube.model.Avatar
|
||||
import net.schueller.peertube.model.Category
|
||||
import net.schueller.peertube.model.Channel
|
||||
import net.schueller.peertube.model.Video
|
||||
import com.mikepenz.iconics.Iconics.Builder
|
||||
import net.schueller.peertube.R.id
|
||||
import net.schueller.peertube.R.menu
|
||||
import net.schueller.peertube.databinding.ItemTagTitleBinding
|
||||
import net.schueller.peertube.databinding.*
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment
|
||||
import net.schueller.peertube.intents.Intents
|
||||
import net.schueller.peertube.model.TagVideo
|
||||
import net.schueller.peertube.model.*
|
||||
import net.schueller.peertube.model.ui.VideoMetaViewItem
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import net.schueller.peertube.network.Session
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.network.GetUserService
|
||||
|
||||
|
||||
sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
var videoRating: Rating? = null
|
||||
var isLeaveAppExpected = false
|
||||
|
||||
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) {
|
||||
fun bind(commentThread: CommentThread) {
|
||||
|
||||
binding.videoCommentsTotalCount.text = commentThread.total.toString()
|
||||
|
||||
if (commentThread.comments.isNotEmpty()) {
|
||||
val highlightedComment: Comment = commentThread.comments[0]
|
||||
|
||||
// owner / creator Avatar
|
||||
val avatar = highlightedComment.account.avatar
|
||||
if (avatar != null) {
|
||||
val baseUrl = APIUrlHelper.getUrl(binding.videoHighlightedAvatar.context)
|
||||
val avatarPath = avatar.path
|
||||
Picasso.get()
|
||||
.load(baseUrl + avatarPath)
|
||||
.into(binding.videoHighlightedAvatar)
|
||||
}
|
||||
binding.videoHighlightedComment.text = highlightedComment.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?): MultiViewRecyclerViewHolder(binding) {
|
||||
fun bind(videoMetaViewItem: VideoMetaViewItem) {
|
||||
|
||||
val video = videoMetaViewItem.video
|
||||
|
||||
if (video != null && videoMetaDataFragment != null) {
|
||||
|
||||
val context = binding.avatar.context
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val userService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetUserService::class.java
|
||||
)
|
||||
|
||||
// Title
|
||||
binding.videoName.text = video.name
|
||||
binding.videoOpenDescription.setOnClickListener {
|
||||
videoMetaDataFragment.showDescriptionFragment(video)
|
||||
}
|
||||
|
||||
// Thumbs up
|
||||
binding.videoThumbsUpWrapper.setOnClickListener {
|
||||
rateVideo(true, video, context, binding)
|
||||
}
|
||||
|
||||
// Thumbs Down
|
||||
binding.videoThumbsDownWrapper.setOnClickListener {
|
||||
rateVideo(false, video, context, binding)
|
||||
}
|
||||
|
||||
binding.videoAddToPlaylistWrapper.setOnClickListener {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_feature_not_yet_implemented),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
binding.videoBlockWrapper.setOnClickListener {
|
||||
Toast.makeText(
|
||||
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
|
||||
).show()
|
||||
}
|
||||
|
||||
// video rating
|
||||
videoRating = Rating()
|
||||
videoRating!!.rating = RATING_NONE // default
|
||||
updateVideoRating(video, binding)
|
||||
|
||||
// Retrieve which rating the user gave to this video
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
val call = videoDataService.getVideoRating(video.id)
|
||||
call.enqueue(object : Callback<Rating?> {
|
||||
override fun onResponse(call: Call<Rating?>, response: Response<Rating?>) {
|
||||
videoRating = response.body()
|
||||
updateVideoRating(video, binding)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Rating?>, t: Throwable) {
|
||||
// Do nothing.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Share
|
||||
binding.videoShare.setOnClickListener {
|
||||
isLeaveAppExpected = true
|
||||
Intents.Share(context, video)
|
||||
}
|
||||
|
||||
// hide download if not supported by video
|
||||
if (video.downloadEnabled) {
|
||||
binding.videoDownloadWrapper.setOnClickListener {
|
||||
Intents.Download(context, video)
|
||||
}
|
||||
} else {
|
||||
binding.videoDownloadWrapper.visibility = GONE
|
||||
}
|
||||
|
||||
val account = video.account
|
||||
|
||||
// owner / creator Avatar
|
||||
val avatar = account.avatar
|
||||
if (avatar != null) {
|
||||
val baseUrl = APIUrlHelper.getUrl(context)
|
||||
val avatarPath = avatar.path
|
||||
Picasso.get()
|
||||
.load(baseUrl + avatarPath)
|
||||
.into(binding.avatar)
|
||||
}
|
||||
// created at / views
|
||||
binding.videoMeta.text = getMetaString(
|
||||
video.createdAt,
|
||||
video.views,
|
||||
context!!
|
||||
)
|
||||
|
||||
// owner / creator
|
||||
binding.videoOwner.text = getOwnerString(
|
||||
video.account.name,
|
||||
video.account.host,
|
||||
context
|
||||
)
|
||||
|
||||
// videoOwnerSubscribers
|
||||
binding.videoOwnerSubscribers.text = video.account.followersCount.toString()
|
||||
|
||||
|
||||
// get subscription status
|
||||
var isSubscribed = false
|
||||
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
val subChannel = video.channel.name + "@" + video.channel.host
|
||||
val call = userService.subscriptionsExist(subChannel)
|
||||
call.enqueue(object : Callback<JsonObject> {
|
||||
override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
|
||||
if (response.isSuccessful) {
|
||||
// {"video.channel.name + "@" + video.channel.host":true}
|
||||
if (response.body()?.get(video.channel.name + "@" + video.channel.host)!!.asBoolean) {
|
||||
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
|
||||
isSubscribed = true;
|
||||
} else {
|
||||
binding.videoOwnerSubscribeButton.setText(string.subscribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
|
||||
// Do nothing.
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
binding.videoOwnerSubscribeButton.setOnClickListener {
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
if (!isSubscribed) {
|
||||
val payload = video.channel.name + "@" + video.channel.host
|
||||
val body = "{\"uri\":\"$payload\"}".toRequestBody("application/json".toMediaType())
|
||||
val call = userService.subscribe(body)
|
||||
call.enqueue(object : Callback<ResponseBody?> {
|
||||
override fun onResponse(
|
||||
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 {
|
||||
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.
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_login_required_for_service),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelViewHolder(private val binding: ItemChannelTitleBinding): MultiViewRecyclerViewHolder(binding) {
|
||||
fun bind(channel: Channel) {
|
||||
|
||||
|
@ -178,4 +417,117 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
|
|||
}
|
||||
|
||||
|
||||
fun updateVideoRating(video: Video?, binding: ItemVideoMetaBinding) {
|
||||
|
||||
when (videoRating!!.rating) {
|
||||
RATING_NONE -> {
|
||||
Log.v("MWCVH", "RATING_NONE")
|
||||
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
|
||||
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
|
||||
}
|
||||
RATING_LIKE -> {
|
||||
Log.v("MWCVH", "RATING_LIKE")
|
||||
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up_filled)
|
||||
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
|
||||
}
|
||||
RATING_DISLIKE -> {
|
||||
Log.v("MWCVH", "RATING_DISLIKE")
|
||||
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
|
||||
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down_filled)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the texts
|
||||
binding.videoThumbsUpTotal.text = video?.likes.toString()
|
||||
binding.videoThumbsDownTotal.text = video?.dislikes.toString()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: move this out and get update when rating changes
|
||||
*/
|
||||
fun rateVideo(like: Boolean, video: Video, context: Context, binding: ItemVideoMetaBinding) {
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
val ratePayload: String = when (videoRating!!.rating) {
|
||||
RATING_LIKE -> if (like) RATING_NONE else RATING_DISLIKE
|
||||
RATING_DISLIKE -> if (like) RATING_LIKE else RATING_NONE
|
||||
RATING_NONE -> if (like) RATING_LIKE else RATING_DISLIKE
|
||||
else -> if (like) RATING_LIKE else RATING_DISLIKE
|
||||
}
|
||||
|
||||
val body = "{\"rating\":\"$ratePayload\"}".toRequestBody("application/json".toMediaType())
|
||||
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL, APIUrlHelper.useInsecureConnection(
|
||||
context
|
||||
)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val call = videoDataService.rateVideo(video.id, body)
|
||||
call.enqueue(object : Callback<ResponseBody?> {
|
||||
override fun onResponse(
|
||||
call: Call<ResponseBody?>,
|
||||
response: Response<ResponseBody?>
|
||||
) {
|
||||
// if 20x, update likes/dislikes
|
||||
if (response.isSuccessful) {
|
||||
val previousRating = videoRating!!.rating
|
||||
|
||||
// Update the likes/dislikes count of the video, if needed.
|
||||
// This is only a visual trick, as the actual like/dislike count has
|
||||
// already been modified on the PeerTube instance.
|
||||
if (previousRating != ratePayload) {
|
||||
when (previousRating) {
|
||||
RATING_NONE -> if (ratePayload == RATING_LIKE) {
|
||||
video.likes = video.likes + 1
|
||||
} else {
|
||||
video.dislikes = video.dislikes + 1
|
||||
}
|
||||
RATING_LIKE -> {
|
||||
video.likes = video.likes - 1
|
||||
if (ratePayload == RATING_DISLIKE) {
|
||||
video.dislikes = video.dislikes + 1
|
||||
}
|
||||
}
|
||||
RATING_DISLIKE -> {
|
||||
video.dislikes = video.dislikes - 1
|
||||
if (ratePayload == RATING_LIKE) {
|
||||
video.likes = video.likes + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
videoRating!!.rating = ratePayload
|
||||
updateVideoRating(video, binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
|
||||
Toast.makeText(
|
||||
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
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RATING_NONE = "none"
|
||||
private const val RATING_LIKE = "like"
|
||||
private const val RATING_DISLIKE = "dislike"
|
||||
const val EXTRA_VIDEOID = "VIDEOID"
|
||||
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import android.os.Parcelable
|
|||
import androidx.room.PrimaryKey
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "server_table")
|
||||
|
|
|
@ -17,7 +17,6 @@
|
|||
package net.schueller.peertube.database
|
||||
|
||||
import android.app.Application
|
||||
import android.os.AsyncTask
|
||||
import androidx.lifecycle.LiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
|
|
@ -19,12 +19,13 @@ package net.schueller.peertube.fragment
|
|||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import net.schueller.peertube.R
|
||||
|
@ -52,7 +53,7 @@ class AddServerFragment : Fragment() {
|
|||
}
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
mBinding = FragmentAddServerBinding.inflate(inflater, container, false)
|
||||
return mBinding.root
|
||||
}
|
||||
|
@ -115,7 +116,7 @@ class AddServerFragment : Fragment() {
|
|||
|
||||
mBinding.pickServerUrl.setOnClickListener {
|
||||
val intentServer = Intent(activity, SearchServerActivity::class.java)
|
||||
this.startActivityForResult(intentServer, PICK_SERVER)
|
||||
openActivityForResult(intentServer)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -132,35 +133,24 @@ class AddServerFragment : Fragment() {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode != PICK_SERVER) {
|
||||
return
|
||||
}
|
||||
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return
|
||||
}
|
||||
|
||||
val serverUrlTest = data?.getStringExtra("serverUrl")
|
||||
//Log.d(TAG, "serverUrl " + serverUrlTest);
|
||||
|
||||
mBinding.serverUrl.setText(serverUrlTest)
|
||||
|
||||
mBinding.serverLabel.apply {
|
||||
if (text.toString().isBlank()) {
|
||||
setText(data?.getStringExtra("serverName"))
|
||||
private var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val intent = result.data
|
||||
val serverUrlTest = intent?.getStringExtra("serverUrl")
|
||||
mBinding.serverUrl.setText(serverUrlTest)
|
||||
mBinding.serverLabel.apply {
|
||||
if (text.toString().isBlank()) {
|
||||
setText(intent?.getStringExtra("serverName"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openActivityForResult(intent: Intent) {
|
||||
resultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AddServerFragment"
|
||||
private const val PICK_SERVER = 1
|
||||
|
||||
private const val SERVER_ARG = "server"
|
||||
|
||||
fun newInstance(server: Server) = AddServerFragment().apply {
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* 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.fragment
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import net.schueller.peertube.R
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.helper.ErrorHelper
|
||||
import net.schueller.peertube.model.Description
|
||||
import net.schueller.peertube.model.Video
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class VideoDescriptionFragment : Fragment () {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_video_description, container,
|
||||
false
|
||||
)
|
||||
|
||||
val video = video
|
||||
|
||||
if (video != null) {
|
||||
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
|
||||
// description, get extended if available
|
||||
val videoDescription = view.findViewById<TextView>(R.id.description)
|
||||
val shortDescription = video.description
|
||||
if (shortDescription != null && shortDescription.length > 237) {
|
||||
val call = videoDataService.getVideoFullDescription(video.uuid);
|
||||
call.enqueue(object : Callback<Description?> {
|
||||
override fun onResponse(call: Call<Description?>, response: Response<Description?>) {
|
||||
val videoFullDescription: Description? = response.body();
|
||||
|
||||
videoDescription.text = videoFullDescription?.description
|
||||
}
|
||||
override fun onFailure(call: Call<Description?>, t: Throwable) {
|
||||
Log.wtf(TAG, t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(activity, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
videoDescription.text = shortDescription;
|
||||
|
||||
val closeButton = view.findViewById<ImageButton>(R.id.video_description_close_button)
|
||||
closeButton.setOnClickListener {
|
||||
videoMetaDataFragment!!.hideDescriptionFragment()
|
||||
}
|
||||
|
||||
// video privacy
|
||||
val videoPrivacy = view.findViewById<TextView>(R.id.video_privacy);
|
||||
videoPrivacy.text = video!!.privacy.label;
|
||||
|
||||
// video category
|
||||
val videoCategory = view.findViewById<TextView>(R.id.video_category);
|
||||
videoCategory.text = video!!.category.label;
|
||||
|
||||
// video privacy
|
||||
val videoLicense = view.findViewById<TextView>(R.id.video_license);
|
||||
videoLicense.text = video!!.licence.label;
|
||||
|
||||
// video language
|
||||
val videoLanguage = view.findViewById<TextView>(R.id.video_language);
|
||||
videoLanguage.text = video!!.language.label;
|
||||
|
||||
// video privacy
|
||||
val videoTags = view.findViewById<TextView>(R.id.video_tags);
|
||||
videoTags.text = android.text.TextUtils.join(", ", video!!.tags);
|
||||
}
|
||||
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var video: Video? = null
|
||||
private var videoMetaDataFragment: VideoMetaDataFragment? = null
|
||||
const val TAG = "VideoDescr"
|
||||
fun newInstance(mVideo: Video?, mVideoMetaDataFragment: VideoMetaDataFragment): VideoDescriptionFragment {
|
||||
video = mVideo
|
||||
videoMetaDataFragment = mVideoMetaDataFragment
|
||||
return VideoDescriptionFragment()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,408 +0,0 @@
|
|||
/*
|
||||
* 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.fragment;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
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 android.widget.Toast;
|
||||
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.helper.ErrorHelper;
|
||||
import net.schueller.peertube.helper.MetaDataHelper;
|
||||
import net.schueller.peertube.intents.Intents;
|
||||
import net.schueller.peertube.model.Account;
|
||||
import net.schueller.peertube.model.Avatar;
|
||||
import net.schueller.peertube.model.Description;
|
||||
import net.schueller.peertube.model.Rating;
|
||||
import net.schueller.peertube.model.Video;
|
||||
import net.schueller.peertube.network.GetVideoDataService;
|
||||
import net.schueller.peertube.network.RetrofitInstance;
|
||||
import net.schueller.peertube.network.Session;
|
||||
import net.schueller.peertube.service.VideoPlayerService;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class VideoMetaDataFragment extends Fragment {
|
||||
|
||||
private static final String TAG = "VideoMetaDataFragment";
|
||||
|
||||
private static final String RATING_NONE = "none";
|
||||
private static final String RATING_LIKE = "like";
|
||||
private static final String RATING_DISLIKE = "dislike";
|
||||
|
||||
private Rating videoRating;
|
||||
private ColorStateList defaultTextColor;
|
||||
|
||||
private boolean leaveAppExpected = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_meta, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause()
|
||||
{
|
||||
leaveAppExpected = false;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public boolean isLeaveAppExpected()
|
||||
{
|
||||
return leaveAppExpected;
|
||||
}
|
||||
|
||||
public void updateVideoMeta(Video video, VideoPlayerService mService) {
|
||||
Context context = getContext();
|
||||
Activity activity = getActivity();
|
||||
|
||||
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
|
||||
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
|
||||
|
||||
// Thumbs up
|
||||
Button thumbsUpButton = activity.findViewById(R.id.video_thumbs_up);
|
||||
defaultTextColor = thumbsUpButton.getTextColors();
|
||||
thumbsUpButton.setText(R.string.video_thumbs_up_icon);
|
||||
new Iconics.Builder().on(thumbsUpButton).build();
|
||||
thumbsUpButton.setOnClickListener(v -> {
|
||||
rateVideo(true, video);
|
||||
});
|
||||
|
||||
// Thumbs Down
|
||||
Button thumbsDownButton = activity.findViewById(R.id.video_thumbs_down);
|
||||
thumbsDownButton.setText(R.string.video_thumbs_down_icon);
|
||||
new Iconics.Builder().on(thumbsDownButton).build();
|
||||
thumbsDownButton.setOnClickListener(v -> {
|
||||
rateVideo(false, video);
|
||||
});
|
||||
|
||||
// video rating
|
||||
videoRating = new Rating();
|
||||
videoRating.setRating(RATING_NONE); // default
|
||||
updateVideoRating(video);
|
||||
|
||||
// Retrieve which rating the user gave to this video
|
||||
if (Session.getInstance().isLoggedIn()) {
|
||||
Call<Rating> call = videoDataService.getVideoRating(video.getId());
|
||||
call.enqueue(new Callback<Rating>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(Call<Rating> call, Response<Rating> response) {
|
||||
videoRating = response.body();
|
||||
updateVideoRating(video);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Rating> call, Throwable t) {
|
||||
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
|
||||
// Do nothing.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Share
|
||||
Button videoShareButton = activity.findViewById(R.id.video_share);
|
||||
videoShareButton.setText(R.string.video_share_icon);
|
||||
new Iconics.Builder().on(videoShareButton).build();
|
||||
videoShareButton.setOnClickListener(v ->
|
||||
{
|
||||
leaveAppExpected = true;
|
||||
Intents.Share( context, video );
|
||||
} );
|
||||
|
||||
// Download
|
||||
Button videoDownloadButton = activity.findViewById(R.id.video_download);
|
||||
videoDownloadButton.setText(R.string.video_download_icon);
|
||||
new Iconics.Builder().on(videoDownloadButton).build();
|
||||
videoDownloadButton.setOnClickListener(v -> {
|
||||
// get permission to store file
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
leaveAppExpected = true;
|
||||
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
Intents.Download(context, video);
|
||||
} else {
|
||||
Toast.makeText(context, getString(R.string.video_download_permission_error), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
Intents.Download(context, video);
|
||||
}
|
||||
});
|
||||
|
||||
Account account = video.getAccount();
|
||||
|
||||
// owner / creator Avatar
|
||||
Avatar avatar = account.getAvatar();
|
||||
if (avatar != null) {
|
||||
ImageView avatarView = activity.findViewById(R.id.avatar);
|
||||
String baseUrl = APIUrlHelper.getUrl(context);
|
||||
String avatarPath = avatar.getPath();
|
||||
Picasso.get()
|
||||
.load(baseUrl + avatarPath)
|
||||
.into(avatarView);
|
||||
}
|
||||
|
||||
|
||||
// title / name
|
||||
TextView videoName = activity.findViewById(R.id.sl_row_name);
|
||||
videoName.setText(video.getName());
|
||||
|
||||
// created at / views
|
||||
TextView videoMeta = activity.findViewById(R.id.videoMeta);
|
||||
videoMeta.setText(
|
||||
MetaDataHelper.getMetaString(
|
||||
video.getCreatedAt(),
|
||||
video.getViews(),
|
||||
context
|
||||
)
|
||||
);
|
||||
|
||||
// owner / creator
|
||||
TextView videoOwner = activity.findViewById(R.id.videoOwner);
|
||||
videoOwner.setText(
|
||||
MetaDataHelper.getOwnerString(video.getAccount().getName(),
|
||||
video.getAccount().getHost(),
|
||||
context
|
||||
)
|
||||
);
|
||||
|
||||
// description
|
||||
TextView videoDescription = activity.findViewById(R.id.description);
|
||||
String shortDescription = video.getDescription();
|
||||
if (shortDescription != null && Objects.requireNonNull(shortDescription).length() > 237) {
|
||||
shortDescription += "\n" + getString(R.string.video_description_read_more);
|
||||
videoDescription.setOnClickListener(v -> {
|
||||
Call<Description> call = videoDataService.getVideoFullDescription(video.getUuid());
|
||||
call.enqueue(new Callback<Description>() {
|
||||
@Override
|
||||
public void onResponse(Call<Description> call, Response<Description> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
new Description();
|
||||
Description videoFullDescription;
|
||||
videoFullDescription = response.body();
|
||||
videoDescription.setText(videoFullDescription.getDescription());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(Call<Description> call, Throwable t) {
|
||||
Toast.makeText(getContext(), getString(R.string.video_get_full_description_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
videoDescription.setText(shortDescription);
|
||||
|
||||
// video privacy
|
||||
TextView videoPrivacy = activity.findViewById(R.id.video_privacy);
|
||||
videoPrivacy.setText(video.getPrivacy().getLabel());
|
||||
|
||||
// video category
|
||||
TextView videoCategory = activity.findViewById(R.id.video_category);
|
||||
videoCategory.setText(video.getCategory().getLabel());
|
||||
|
||||
// video privacy
|
||||
TextView videoLicense = activity.findViewById(R.id.video_license);
|
||||
videoLicense.setText(video.getLicence().getLabel());
|
||||
|
||||
// video language
|
||||
TextView videoLanguage = activity.findViewById(R.id.video_language);
|
||||
videoLanguage.setText(video.getLanguage().getLabel());
|
||||
|
||||
// video privacy
|
||||
TextView videoTags = activity.findViewById(R.id.video_tags);
|
||||
videoTags.setText(android.text.TextUtils.join(", ", video.getTags()));
|
||||
|
||||
// more button
|
||||
TextView moreButton = activity.findViewById(R.id.moreButton);
|
||||
moreButton.setText(R.string.video_more_icon);
|
||||
new Iconics.Builder().on(moreButton).build();
|
||||
|
||||
moreButton.setOnClickListener(v -> {
|
||||
PopupMenu popup = new PopupMenu(context, v);
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.video_more_report:
|
||||
Log.v(TAG, "Report");
|
||||
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case R.id.video_more_blacklist:
|
||||
Log.v(TAG, "Blacklist");
|
||||
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
popup.inflate(R.menu.menu_video_more);
|
||||
popup.show();
|
||||
});
|
||||
|
||||
// video player options
|
||||
TextView videoOptions = activity.findViewById(R.id.exo_more);
|
||||
videoOptions.setText(R.string.video_more_icon);
|
||||
new Iconics.Builder().on(videoOptions).build();
|
||||
|
||||
videoOptions.setOnClickListener(v -> {
|
||||
VideoOptionsFragment videoOptionsFragment =
|
||||
VideoOptionsFragment.newInstance(mService, video.getFiles());
|
||||
videoOptionsFragment.show(getActivity().getSupportFragmentManager(),
|
||||
VideoOptionsFragment.TAG);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void updateVideoRating(Video video) {
|
||||
Button thumbsUpButton = getActivity().findViewById(R.id.video_thumbs_up);
|
||||
Button thumbsDownButton = getActivity().findViewById(R.id.video_thumbs_down);
|
||||
|
||||
TypedValue typedValue = new TypedValue();
|
||||
|
||||
TypedArray a = getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorPrimary});
|
||||
int accentColor = a.getColor(0, 0);
|
||||
|
||||
// Change the color of the thumbs
|
||||
switch (videoRating.getRating()) {
|
||||
case RATING_NONE:
|
||||
thumbsUpButton.setTextColor(defaultTextColor);
|
||||
thumbsDownButton.setTextColor(defaultTextColor);
|
||||
break;
|
||||
case RATING_LIKE:
|
||||
thumbsUpButton.setTextColor(accentColor);
|
||||
thumbsDownButton.setTextColor(defaultTextColor);
|
||||
break;
|
||||
case RATING_DISLIKE:
|
||||
thumbsUpButton.setTextColor(defaultTextColor);
|
||||
thumbsDownButton.setTextColor(accentColor);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the texts
|
||||
TextView thumbsDownTotal = getActivity().findViewById(R.id.video_thumbs_down_total);
|
||||
TextView thumbsUpTotal = getActivity().findViewById(R.id.video_thumbs_up_total);
|
||||
thumbsUpTotal.setText(String.valueOf(video.getLikes()));
|
||||
thumbsDownTotal.setText(String.valueOf(video.getDislikes()));
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
void rateVideo(Boolean like, Video video) {
|
||||
if (Session.getInstance().isLoggedIn()) {
|
||||
final String ratePayload;
|
||||
|
||||
switch (videoRating.getRating()) {
|
||||
case RATING_LIKE:
|
||||
ratePayload = like ? RATING_NONE : RATING_DISLIKE;
|
||||
break;
|
||||
case RATING_DISLIKE:
|
||||
ratePayload = like ? RATING_LIKE : RATING_NONE;
|
||||
break;
|
||||
case RATING_NONE:
|
||||
default:
|
||||
ratePayload = like ? RATING_LIKE : RATING_DISLIKE;
|
||||
break;
|
||||
}
|
||||
|
||||
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"), "{\"rating\":\"" + ratePayload + "\"}");
|
||||
|
||||
String apiBaseURL = APIUrlHelper.getUrlWithVersion(getContext());
|
||||
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(getContext())).create(GetVideoDataService.class);
|
||||
|
||||
Call<ResponseBody> call = videoDataService.rateVideo(video.getId(), body);
|
||||
|
||||
call.enqueue(new Callback<ResponseBody>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
|
||||
//Log.v(TAG, response.toString());
|
||||
|
||||
// if 20x, update likes/dislikes
|
||||
if (response.isSuccessful()) {
|
||||
String previousRating = videoRating.getRating();
|
||||
|
||||
// Update the likes/dislikes count of the video, if needed.
|
||||
// This is only a visual trick, as the actual like/dislike count has
|
||||
// already been modified on the PeerTube instance.
|
||||
if (!previousRating.equals(ratePayload)) {
|
||||
switch (previousRating) {
|
||||
case RATING_NONE:
|
||||
if (ratePayload.equals(RATING_LIKE)) {
|
||||
video.setLikes(video.getLikes() + 1);
|
||||
} else {
|
||||
video.setDislikes(video.getDislikes() + 1);
|
||||
}
|
||||
break;
|
||||
case RATING_LIKE:
|
||||
video.setLikes(video.getLikes() - 1);
|
||||
if (ratePayload.equals(RATING_DISLIKE)) {
|
||||
video.setDislikes(video.getDislikes() + 1);
|
||||
}
|
||||
break;
|
||||
case RATING_DISLIKE:
|
||||
video.setDislikes(video.getDislikes() - 1);
|
||||
if (ratePayload.equals(RATING_LIKE)) {
|
||||
video.setLikes(video.getLikes() + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
videoRating.setRating(ratePayload);
|
||||
updateVideoRating(video);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Toast.makeText(getContext(), getString(R.string.video_rating_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Toast.makeText(getContext(), getString(R.string.video_login_required_for_service), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,240 @@
|
|||
/*
|
||||
* 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.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.util.Log
|
||||
import android.widget.Toast
|
||||
import com.squareup.picasso.Picasso
|
||||
import android.widget.TextView
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mikepenz.iconics.Iconics
|
||||
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
|
||||
import net.schueller.peertube.intents.Intents
|
||||
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 retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.Exception
|
||||
|
||||
class VideoMetaDataFragment : Fragment() {
|
||||
private var videoRating: Rating? = null
|
||||
private var defaultTextColor: ColorStateList? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var mMultiViewAdapter: MultiViewRecycleViewAdapter? = null
|
||||
|
||||
private lateinit var videoDescriptionFragment: VideoDescriptionFragment
|
||||
|
||||
var isLeaveAppExpected = false
|
||||
private set
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_meta, container, false)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
isLeaveAppExpected = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
fun showDescriptionFragment(video: Video) {
|
||||
// show full description fragment
|
||||
videoDescriptionFragment = VideoDescriptionFragment.newInstance(video, this)
|
||||
childFragmentManager.beginTransaction()
|
||||
.add(R.id.video_meta_data_fragment, videoDescriptionFragment, VideoDescriptionFragment.TAG).commit()
|
||||
}
|
||||
|
||||
fun hideDescriptionFragment() {
|
||||
val fragment: Fragment? = childFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
|
||||
if (fragment != null) {
|
||||
childFragmentManager.beginTransaction().remove(fragment).commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateVideoMeta(video: Video, mService: VideoPlayerService?) {
|
||||
|
||||
// Remove description if it is open as we are loading a new video
|
||||
hideDescriptionFragment()
|
||||
|
||||
val context = context
|
||||
val activity: Activity? = activity
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
|
||||
// related videos
|
||||
recyclerView = activity!!.findViewById(R.id.relatedVideosView)
|
||||
val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this@VideoMetaDataFragment.context)
|
||||
recyclerView?.layoutManager = layoutManager
|
||||
mMultiViewAdapter = MultiViewRecycleViewAdapter(this)
|
||||
recyclerView?.adapter = mMultiViewAdapter
|
||||
|
||||
val videoMetaViewItem = VideoMetaViewItem()
|
||||
videoMetaViewItem.video = video
|
||||
mMultiViewAdapter?.setVideoMeta(videoMetaViewItem)
|
||||
|
||||
loadVideos()
|
||||
|
||||
// loadComments(video.id)
|
||||
|
||||
// mMultiViewAdapter?.setVideoComment()
|
||||
|
||||
// videoOwnerSubscribeButton
|
||||
|
||||
|
||||
// description
|
||||
|
||||
|
||||
// video player options
|
||||
val videoOptions = activity.findViewById<TextView>(R.id.exo_more)
|
||||
videoOptions.setText(R.string.video_more_icon)
|
||||
Iconics.Builder().on(videoOptions).build()
|
||||
videoOptions.setOnClickListener {
|
||||
val videoOptionsFragment = VideoOptionsFragment.newInstance(mService, video.files)
|
||||
videoOptionsFragment.show(
|
||||
getActivity()!!.supportFragmentManager,
|
||||
VideoOptionsFragment.TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadComments(videoId: Int) {
|
||||
val context = context
|
||||
|
||||
val start = 0
|
||||
val count = 1
|
||||
val sort = "-createdAt"
|
||||
|
||||
|
||||
// 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
|
||||
)
|
||||
val call: Call<CommentThread> = service.getCommentThreads(videoId, start, count, sort)
|
||||
|
||||
call.enqueue(object : Callback<CommentThread?> {
|
||||
override fun onResponse(call: Call<CommentThread?>, response: Response<CommentThread?>) {
|
||||
if (response.body() != null) {
|
||||
val commentThread = response.body()
|
||||
if (commentThread != null) {
|
||||
mMultiViewAdapter!!.setVideoComment(commentThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<CommentThread?>, t: Throwable) {
|
||||
Log.wtf("err", t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun loadVideos() {
|
||||
val context = context
|
||||
|
||||
val start = 0
|
||||
val count = 6
|
||||
val sort = "-createdAt"
|
||||
val filter: String? = null
|
||||
|
||||
val sharedPref = context?.getSharedPreferences(
|
||||
context.packageName + "_preferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
var nsfw = "false"
|
||||
var languages: Set<String>? = emptySet()
|
||||
if (sharedPref != null) {
|
||||
nsfw = if (sharedPref.getBoolean(getString(R.string.pref_show_nsfw_key), false)) "both" else "false"
|
||||
languages = sharedPref.getStringSet(getString(R.string.pref_video_language_key), null)
|
||||
}
|
||||
|
||||
// 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
|
||||
)
|
||||
val call: Call<VideoList> = service.getVideosData(start, count, sort, nsfw, filter, languages)
|
||||
|
||||
/*Log the URL called*/Log.d("URL Called", call.request().url.toString() + "")
|
||||
// Toast.makeText(VideoListActivity.this, "URL Called: " + call.request().url(), Toast.LENGTH_SHORT).show();
|
||||
call.enqueue(object : Callback<VideoList?> {
|
||||
override fun onResponse(call: Call<VideoList?>, response: Response<VideoList?>) {
|
||||
if (response.body() != null) {
|
||||
val videoList = response.body()
|
||||
if (videoList != null) {
|
||||
mMultiViewAdapter!!.setVideoListData(videoList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<VideoList?>, t: Throwable) {
|
||||
Log.wtf("err", t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
companion object {
|
||||
const val TAG = "VMDF"
|
||||
}
|
||||
}
|
|
@ -1,516 +0,0 @@
|
|||
/*
|
||||
* 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.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.se_bastiaan.torrentstream.StreamStatus;
|
||||
import com.github.se_bastiaan.torrentstream.Torrent;
|
||||
import com.github.se_bastiaan.torrentstream.TorrentOptions;
|
||||
import com.github.se_bastiaan.torrentstream.TorrentStream;
|
||||
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
|
||||
import net.schueller.peertube.R;
|
||||
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.helper.ErrorHelper;
|
||||
import net.schueller.peertube.model.File;
|
||||
import net.schueller.peertube.model.Video;
|
||||
import net.schueller.peertube.network.GetVideoDataService;
|
||||
import net.schueller.peertube.network.RetrofitInstance;
|
||||
import net.schueller.peertube.service.VideoPlayerService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
|
||||
|
||||
public class VideoPlayerFragment extends Fragment implements VideoRendererEventListener {
|
||||
|
||||
private String mVideoUuid;
|
||||
private ProgressBar progressBar;
|
||||
private PlayerView simpleExoPlayerView;
|
||||
private Intent videoPlayerIntent;
|
||||
private Boolean mBound = false;
|
||||
private Boolean isFullscreen = false;
|
||||
private VideoPlayerService mService;
|
||||
private TorrentStream torrentStream;
|
||||
private LinearLayout torrentStatus;
|
||||
private float aspectRatio;
|
||||
|
||||
private static final String TAG = "VideoPlayerFragment";
|
||||
private GestureDetector mDetector;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
Log.d(TAG, "onServiceConnected");
|
||||
VideoPlayerService.LocalBinder binder = (VideoPlayerService.LocalBinder) service;
|
||||
mService = binder.getService();
|
||||
|
||||
// 2. Create the player
|
||||
simpleExoPlayerView.setPlayer(mService.player);
|
||||
mBound = true;
|
||||
|
||||
loadVideo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
Log.d(TAG, "onServiceDisconnected");
|
||||
simpleExoPlayerView.setPlayer(null);
|
||||
mBound = false;
|
||||
}
|
||||
};
|
||||
private AspectRatioFrameLayout.AspectRatioListener aspectRatioListerner = new AspectRatioFrameLayout.AspectRatioListener()
|
||||
{
|
||||
@Override
|
||||
public void onAspectRatioUpdated( float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch )
|
||||
{
|
||||
aspectRatio = targetAspectRatio;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_player, container, false);
|
||||
}
|
||||
|
||||
|
||||
public void start(String videoUuid) {
|
||||
|
||||
// start service
|
||||
Context context = getContext();
|
||||
Activity activity = getActivity();
|
||||
|
||||
mVideoUuid = videoUuid;
|
||||
|
||||
assert activity != null;
|
||||
progressBar = activity.findViewById(R.id.torrent_progress);
|
||||
progressBar.setMax(100);
|
||||
|
||||
assert context != null;
|
||||
simpleExoPlayerView = new PlayerView(context);
|
||||
simpleExoPlayerView = activity.findViewById(R.id.video_view);
|
||||
|
||||
simpleExoPlayerView.setControllerShowTimeoutMs(1000);
|
||||
simpleExoPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
|
||||
|
||||
mDetector = new GestureDetector(context, new MyGestureListener());
|
||||
simpleExoPlayerView.setOnTouchListener(touchListener);
|
||||
|
||||
simpleExoPlayerView.setAspectRatioListener( aspectRatioListerner );
|
||||
|
||||
torrentStatus = activity.findViewById(R.id.exo_torrent_status);
|
||||
|
||||
// Full screen Icon
|
||||
TextView fullscreenText = activity.findViewById(R.id.exo_fullscreen);
|
||||
FrameLayout fullscreenButton = activity.findViewById(R.id.exo_fullscreen_button);
|
||||
|
||||
fullscreenText.setText(R.string.video_expand_icon);
|
||||
new Iconics.Builder().on(fullscreenText).build();
|
||||
|
||||
fullscreenButton.setOnClickListener(view -> {
|
||||
Log.d(TAG, "Fullscreen");
|
||||
fullScreenToggle();
|
||||
});
|
||||
|
||||
if (!mBound) {
|
||||
videoPlayerIntent = new Intent(context, VideoPlayerService.class);
|
||||
activity.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void loadVideo() {
|
||||
Context context = getContext();
|
||||
|
||||
|
||||
// get video details from api
|
||||
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
|
||||
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
|
||||
|
||||
Call<Video> call = service.getVideoData(mVideoUuid);
|
||||
|
||||
call.enqueue(new Callback<Video>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Video> call, @NonNull Response<Video> response) {
|
||||
|
||||
Video video = response.body();
|
||||
|
||||
mService.setCurrentVideo(video);
|
||||
|
||||
if (video == null) {
|
||||
Toast.makeText(context, "Unable to retrieve video information, try again later.", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
playVideo(video);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Video> call, @NonNull Throwable t) {
|
||||
Log.wtf(TAG, t.fillInStackTrace());
|
||||
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void useController(boolean value) {
|
||||
if (mBound) {
|
||||
simpleExoPlayerView.setUseController(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void playVideo(Video video) {
|
||||
|
||||
Context context = getContext();
|
||||
|
||||
// video Meta fragment
|
||||
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment)
|
||||
requireActivity().getSupportFragmentManager().findFragmentById(R.id.video_meta_data_fragment);
|
||||
|
||||
assert videoMetaDataFragment != null;
|
||||
videoMetaDataFragment.updateVideoMeta(video, mService);
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
if (sharedPref.getBoolean(getString(R.string.pref_torrent_player_key), false)) {
|
||||
torrentStatus.setVisibility(View.VISIBLE);
|
||||
String stream = video.getFiles().get(0).getTorrentUrl();
|
||||
Log.v(TAG, "getTorrentUrl : " + video.getFiles().get(0).getTorrentUrl());
|
||||
torrentStream = setupTorrentStream();
|
||||
torrentStream.startStream(stream);
|
||||
} else {
|
||||
|
||||
Integer videoQuality = sharedPref.getInt(getString(R.string.pref_quality_key), 999999);
|
||||
|
||||
String urlToPlay = null;
|
||||
boolean isHLS = false;
|
||||
|
||||
// try HLS stream first
|
||||
// get video qualities
|
||||
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
|
||||
if (video.getStreamingPlaylists().size() > 0) {
|
||||
urlToPlay = video.getStreamingPlaylists().get( 0 ).getPlaylistUrl();
|
||||
isHLS = true;
|
||||
} else {
|
||||
if (video.getFiles().size() > 0) {
|
||||
urlToPlay = video.getFiles().get( 0 ).getFileUrl(); // default, take first found, usually highest res
|
||||
for ( File file : video.getFiles() ) {
|
||||
// Set quality if it matches
|
||||
if ( file.getResolution().getId().equals( videoQuality ) ) {
|
||||
urlToPlay = file.getFileUrl();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!urlToPlay.isEmpty()) {
|
||||
mService.setCurrentStreamUrl( urlToPlay, isHLS);
|
||||
torrentStatus.setVisibility(View.GONE);
|
||||
startPlayer();
|
||||
} else {
|
||||
stopVideo();
|
||||
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "end of load Video");
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void startPlayer() {
|
||||
Util.startForegroundService(requireContext(), videoPlayerIntent);
|
||||
}
|
||||
|
||||
|
||||
public void destroyVideo() {
|
||||
simpleExoPlayerView.setPlayer(null);
|
||||
if (torrentStream != null) {
|
||||
torrentStream.stopStream();
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseVideo() {
|
||||
if (mBound) {
|
||||
mService.player.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseToggle() {
|
||||
if (mBound) {
|
||||
mService.player.setPlayWhenReady(!mService.player.getPlayWhenReady());
|
||||
}
|
||||
}
|
||||
|
||||
public void unPauseVideo() {
|
||||
if (mBound) {
|
||||
mService.player.setPlayWhenReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
public float getVideoAspectRatio() { return aspectRatio; }
|
||||
|
||||
public boolean isPaused() {
|
||||
return !mService.player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
public void showControls(boolean value) {
|
||||
simpleExoPlayerView.setUseController(value);
|
||||
}
|
||||
|
||||
public void stopVideo() {
|
||||
|
||||
if (mBound) {
|
||||
requireContext().unbindService(mConnection);
|
||||
mBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsFullscreen(Boolean fullscreen) {
|
||||
isFullscreen = fullscreen;
|
||||
|
||||
TextView fullscreenButton = requireActivity().findViewById(R.id.exo_fullscreen);
|
||||
if (fullscreen) {
|
||||
fullscreenButton.setText(R.string.video_compress_icon);
|
||||
} else {
|
||||
fullscreenButton.setText(R.string.video_expand_icon);
|
||||
}
|
||||
new Iconics.Builder().on(fullscreenButton).build();
|
||||
}
|
||||
|
||||
public Boolean getIsFullscreen() {
|
||||
return isFullscreen;
|
||||
}
|
||||
|
||||
public void fullScreenToggle() {
|
||||
if (!isFullscreen) {
|
||||
setIsFullscreen(true);
|
||||
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else {
|
||||
setIsFullscreen(false);
|
||||
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Torrent Playback
|
||||
*
|
||||
* @return torrent stream
|
||||
*/
|
||||
private TorrentStream setupTorrentStream() {
|
||||
|
||||
TorrentOptions torrentOptions = new TorrentOptions.Builder()
|
||||
.saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
|
||||
.removeFilesAfterStop(true)
|
||||
.build();
|
||||
|
||||
TorrentStream torrentStream = TorrentStream.init(torrentOptions);
|
||||
|
||||
torrentStream.addListener(new TorrentListener() {
|
||||
@Override
|
||||
public void onStreamReady(Torrent torrent) {
|
||||
String videopath = Uri.fromFile(torrent.getVideoFile()).toString();
|
||||
Log.d(TAG, "Ready! torrentStream videopath:" + videopath);
|
||||
mService.setCurrentStreamUrl(videopath, false);
|
||||
startPlayer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamProgress(Torrent torrent, StreamStatus streamStatus) {
|
||||
if (streamStatus.bufferProgress <= 100 && progressBar.getProgress() < 100 && progressBar.getProgress() != streamStatus.bufferProgress) {
|
||||
//Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
|
||||
progressBar.setProgress(streamStatus.bufferProgress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamStopped() {
|
||||
Log.d(TAG, "Stopped");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamPrepared(Torrent torrent) {
|
||||
Log.d(TAG, "Prepared");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamStarted(Torrent torrent) {
|
||||
Log.d(TAG, "Started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamError(Torrent torrent, Exception e) {
|
||||
Log.d(TAG, "Error: " + e.getMessage());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return torrentStream;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onVideoEnabled(DecoderCounters counters) {
|
||||
Log.v(TAG, "onVideoEnabled()...");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoInputFormatChanged(Format format) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsedMs) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame(Surface surface) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoDisabled(DecoderCounters counters) {
|
||||
Log.v(TAG, "onVideoDisabled()...");
|
||||
}
|
||||
|
||||
View.OnTouchListener touchListener = new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
return mDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public String getVideoUuid() {
|
||||
return mVideoUuid;
|
||||
}
|
||||
|
||||
class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
/*
|
||||
@Override
|
||||
public boolean onDown(MotionEvent event) {
|
||||
Log.d("TAG","onDown: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
Log.i("TAG", "onSingleTapConfirmed: ");
|
||||
pauseToggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
Log.i("TAG", "onLongPress: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
Log.i("TAG", "onDoubleTap: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2,
|
||||
float distanceX, float distanceY) {
|
||||
Log.i("TAG", "onScroll: ");
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public boolean onFling(MotionEvent event1, MotionEvent event2,
|
||||
float velocityX, float velocityY) {
|
||||
Log.d(TAG, event1.toString());
|
||||
Log.d(TAG, event2.toString());
|
||||
Log.d(TAG, String.valueOf(velocityX));
|
||||
Log.d(TAG, String.valueOf(velocityY));
|
||||
//arbitrarily velocity speeds that seem to work to differentiate events.
|
||||
if (velocityY > 4000) {
|
||||
Log.d(TAG, "we have a drag down event");
|
||||
if (canEnterPipMode(getContext())) {
|
||||
requireActivity().enterPictureInPictureMode();
|
||||
}
|
||||
}
|
||||
if ((velocityX > 2000) && (Math.abs(velocityY) < 2000)) {
|
||||
Log.d(TAG, "swipe right " + velocityY);
|
||||
}
|
||||
if ((velocityX < 2000) && (Math.abs(velocityY) < 2000)) {
|
||||
Log.d(TAG, "swipe left " + velocityY);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
/*
|
||||
* 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.fragment
|
||||
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener
|
||||
import com.google.android.exoplayer2.ui.PlayerView
|
||||
import android.content.Intent
|
||||
import net.schueller.peertube.service.VideoPlayerService
|
||||
import com.github.se_bastiaan.torrentstream.TorrentStream
|
||||
import android.widget.LinearLayout
|
||||
import android.view.GestureDetector
|
||||
import android.content.ServiceConnection
|
||||
import android.content.ComponentName
|
||||
import android.os.IBinder
|
||||
import net.schueller.peertube.service.VideoPlayerService.LocalBinder
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.AspectRatioListener
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.os.Bundle
|
||||
import net.schueller.peertube.R
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.FrameLayout
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import android.widget.Toast
|
||||
import net.schueller.peertube.helper.ErrorHelper
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener
|
||||
import com.github.se_bastiaan.torrentstream.Torrent
|
||||
import com.github.se_bastiaan.torrentstream.StreamStatus
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.MotionEvent
|
||||
import android.view.GestureDetector.SimpleOnGestureListener
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.se_bastiaan.torrentstream.TorrentOptions.Builder
|
||||
import com.google.android.exoplayer2.Format
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import com.mikepenz.iconics.Iconics
|
||||
import net.schueller.peertube.R.layout
|
||||
import net.schueller.peertube.R.string
|
||||
import net.schueller.peertube.helper.VideoHelper
|
||||
import net.schueller.peertube.model.Video
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.Exception
|
||||
import kotlin.math.abs
|
||||
|
||||
class VideoPlayerFragment : Fragment(), VideoRendererEventListener {
|
||||
|
||||
var videoUuid: String? = null
|
||||
private set
|
||||
// private var progressBar: ProgressBar? = null
|
||||
private var exoPlayer: PlayerView? = null
|
||||
private var videoPlayerIntent: Intent? = null
|
||||
private var mBound = false
|
||||
private var isFullscreen = false
|
||||
private var mService: VideoPlayerService? = null
|
||||
private var torrentStream: TorrentStream? = null
|
||||
// private var torrentStatus: LinearLayout? = null
|
||||
var videoAspectRatio = 0f
|
||||
private set
|
||||
private var mDetector: GestureDetector? = null
|
||||
private val mConnection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
Log.d(TAG, "onServiceConnected")
|
||||
val binder = service as LocalBinder
|
||||
mService = binder.service
|
||||
|
||||
// 2. Create the player
|
||||
exoPlayer!!.player = mService!!.player
|
||||
mBound = true
|
||||
loadVideo()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {
|
||||
Log.d(TAG, "onServiceDisconnected")
|
||||
exoPlayer!!.player = null
|
||||
mBound = false
|
||||
}
|
||||
}
|
||||
private val aspectRatioListener: AspectRatioListener = AspectRatioListener {
|
||||
targetAspectRatio, _, _ -> videoAspectRatio = targetAspectRatio
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(layout.fragment_video_player, container, false)
|
||||
}
|
||||
|
||||
fun start(videoUuid: String?) {
|
||||
|
||||
// start service
|
||||
val context = context
|
||||
val activity: Activity? = activity
|
||||
this.videoUuid = videoUuid
|
||||
assert(activity != null)
|
||||
// progressBar = activity?.findViewById(R.id.torrent_progress)
|
||||
// progressBar?.max = 100
|
||||
assert(context != null)
|
||||
exoPlayer = PlayerView(context!!)
|
||||
exoPlayer = activity?.findViewById(R.id.video_view)
|
||||
exoPlayer?.controllerShowTimeoutMs = 1000
|
||||
exoPlayer?.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
mDetector = GestureDetector(context, MyGestureListener())
|
||||
exoPlayer?.setOnTouchListener(touchListener)
|
||||
exoPlayer?.setAspectRatioListener(aspectRatioListener)
|
||||
// torrentStatus = activity?.findViewById(R.id.exo_torrent_status)
|
||||
|
||||
// Full screen Icon
|
||||
val fullscreenText = activity?.findViewById<TextView>(R.id.exo_fullscreen)
|
||||
val fullscreenButton = activity?.findViewById<FrameLayout>(R.id.exo_fullscreen_button)
|
||||
fullscreenText?.setText(string.video_expand_icon)
|
||||
if (fullscreenText != null) {
|
||||
Iconics.Builder().on(fullscreenText).build()
|
||||
fullscreenButton?.setOnClickListener {
|
||||
Log.d(TAG, "Fullscreen")
|
||||
fullScreenToggle()
|
||||
}
|
||||
}
|
||||
if (!mBound) {
|
||||
videoPlayerIntent = Intent(context, VideoPlayerService::class.java)
|
||||
activity?.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVideo() {
|
||||
val context = context
|
||||
|
||||
// get video details from api
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val service =
|
||||
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val call = service.getVideoData(videoUuid)
|
||||
call.enqueue(object : Callback<Video?> {
|
||||
override fun onResponse(call: Call<Video?>, response: Response<Video?>) {
|
||||
val video = response.body()
|
||||
mService!!.setCurrentVideo(video)
|
||||
if (video == null) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Unable to retrieve video information, try again later.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
playVideo(video)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Video?>, t: Throwable) {
|
||||
Log.wtf(TAG, t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(activity, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun useController(value: Boolean) {
|
||||
if (mBound) {
|
||||
exoPlayer!!.useController = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun playVideo(video: Video) {
|
||||
val context = context
|
||||
|
||||
// video Meta fragment
|
||||
val videoMetaDataFragment =
|
||||
(requireActivity().supportFragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?)!!
|
||||
videoMetaDataFragment.updateVideoMeta(video, mService)
|
||||
|
||||
val sharedPref = context?.getSharedPreferences(
|
||||
context.packageName + "_preferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
var prefTorrentPlayer = false
|
||||
var videoQuality = 999999
|
||||
if (sharedPref != null) {
|
||||
prefTorrentPlayer = sharedPref.getBoolean(getString(string.pref_torrent_player_key), false)
|
||||
videoQuality = sharedPref.getInt(getString(string.pref_quality_key), 999999)
|
||||
}
|
||||
|
||||
// if (prefTorrentPlayer) {
|
||||
// torrentStatus!!.visibility = View.VISIBLE
|
||||
// val stream = video.files[0].torrentUrl
|
||||
// Log.v(TAG, "getTorrentUrl : " + video.files[0].torrentUrl)
|
||||
// torrentStream = setupTorrentStream()
|
||||
// torrentStream!!.startStream(stream)
|
||||
// } else {
|
||||
var urlToPlay: String? = null
|
||||
var isHLS = false
|
||||
|
||||
// try HLS stream first
|
||||
// get video qualities
|
||||
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
|
||||
if (video.streamingPlaylists.size > 0) {
|
||||
urlToPlay = video.streamingPlaylists[0].playlistUrl
|
||||
isHLS = true
|
||||
} else {
|
||||
if (video.files.size > 0) {
|
||||
urlToPlay = video.files[0].fileUrl // default, take first found, usually highest res
|
||||
for (file in video.files) {
|
||||
// Set quality if it matches
|
||||
if (file.resolution.id == videoQuality) {
|
||||
urlToPlay = file.fileUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlToPlay!!.isNotEmpty()) {
|
||||
mService!!.setCurrentStreamUrl(urlToPlay, isHLS)
|
||||
// torrentStatus!!.visibility = View.GONE
|
||||
startPlayer()
|
||||
} else {
|
||||
stopVideo()
|
||||
Toast.makeText(context, string.api_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// }
|
||||
Log.v(TAG, "end of load Video")
|
||||
}
|
||||
|
||||
private fun startPlayer() {
|
||||
Util.startForegroundService(requireContext(), videoPlayerIntent!!)
|
||||
}
|
||||
|
||||
fun destroyVideo() {
|
||||
exoPlayer!!.player = null
|
||||
if (torrentStream != null) {
|
||||
torrentStream!!.stopStream()
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseVideo() {
|
||||
if (mBound) {
|
||||
mService!!.player!!.playWhenReady = false
|
||||
}
|
||||
}
|
||||
|
||||
// fun pauseToggle() {
|
||||
// if (mBound) {
|
||||
// mService!!.player!!.playWhenReady = !mService!!.player!!.playWhenReady
|
||||
// }
|
||||
// }
|
||||
|
||||
fun unPauseVideo() {
|
||||
if (mBound) {
|
||||
mService!!.player!!.playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
val isPaused: Boolean
|
||||
get() = !mService!!.player!!.playWhenReady
|
||||
|
||||
fun showControls(value: Boolean) {
|
||||
exoPlayer!!.useController = value
|
||||
}
|
||||
|
||||
fun stopVideo() {
|
||||
if (mBound) {
|
||||
requireContext().unbindService(mConnection)
|
||||
mBound = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* triggered rotation and button press
|
||||
*/
|
||||
fun setIsFullscreen(fullscreen: Boolean) {
|
||||
isFullscreen = fullscreen
|
||||
val fullscreenButton = requireActivity().findViewById<TextView>(R.id.exo_fullscreen)
|
||||
if (fullscreen) {
|
||||
hideSystemBars()
|
||||
fullscreenButton.setText(string.video_compress_icon)
|
||||
} else {
|
||||
restoreSystemBars()
|
||||
fullscreenButton.setText(string.video_expand_icon)
|
||||
}
|
||||
Iconics.Builder().on(fullscreenButton).build()
|
||||
}
|
||||
|
||||
private fun hideSystemBars()
|
||||
{
|
||||
val view = this.view
|
||||
if (view != null) {
|
||||
val windowInsetsController =
|
||||
ViewCompat.getWindowInsetsController(view) ?: return
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreSystemBars()
|
||||
{
|
||||
val view = this.view
|
||||
if (view != null) {
|
||||
val windowInsetsController =
|
||||
ViewCompat.getWindowInsetsController(view) ?: return
|
||||
// Show both the status bar and the navigation bar
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
fun getIsFullscreen(): Boolean {
|
||||
return isFullscreen
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered by button press
|
||||
*/
|
||||
fun fullScreenToggle() {
|
||||
if (!isFullscreen) {
|
||||
setIsFullscreen(true)
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
} else {
|
||||
setIsFullscreen(false)
|
||||
// we want to force portrait if fullscreen is switched of as we do not have a min. landscape view
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * Torrent Playback
|
||||
// *
|
||||
// * @return torrent stream
|
||||
// */
|
||||
// private fun setupTorrentStream(): TorrentStream {
|
||||
// val torrentOptions = Builder()
|
||||
// .saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
|
||||
// .removeFilesAfterStop(true)
|
||||
// .build()
|
||||
// val torrentStream = TorrentStream.init(torrentOptions)
|
||||
// torrentStream.addListener(object : TorrentListener {
|
||||
// override fun onStreamReady(torrent: Torrent) {
|
||||
// val videoPath = Uri.fromFile(torrent.videoFile).toString()
|
||||
// Log.d(TAG, "Ready! torrentStream videoPath:$videoPath")
|
||||
// mService!!.setCurrentStreamUrl(videoPath, false)
|
||||
// startPlayer()
|
||||
// }
|
||||
//
|
||||
// override fun onStreamProgress(torrent: Torrent, streamStatus: StreamStatus) {
|
||||
// if (streamStatus.bufferProgress <= 100 && progressBar!!.progress < 100 && progressBar!!.progress != streamStatus.bufferProgress) {
|
||||
// //Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
|
||||
// progressBar!!.progress = streamStatus.bufferProgress
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onStreamStopped() {
|
||||
// Log.d(TAG, "Stopped")
|
||||
// }
|
||||
//
|
||||
// override fun onStreamPrepared(torrent: Torrent) {
|
||||
// Log.d(TAG, "Prepared")
|
||||
// }
|
||||
//
|
||||
// override fun onStreamStarted(torrent: Torrent) {
|
||||
// Log.d(TAG, "Started")
|
||||
// }
|
||||
//
|
||||
// override fun onStreamError(torrent: Torrent, e: Exception) {
|
||||
// Log.d(TAG, "Error: " + e.message)
|
||||
// }
|
||||
// })
|
||||
// return torrentStream
|
||||
// }
|
||||
|
||||
override fun onVideoEnabled(counters: DecoderCounters) {
|
||||
Log.v(TAG, "onVideoEnabled()...")
|
||||
}
|
||||
|
||||
override fun onVideoDecoderInitialized(
|
||||
decoderName: String,
|
||||
initializedTimestampMs: Long,
|
||||
initializationDurationMs: Long
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onVideoInputFormatChanged(format: Format) {}
|
||||
override fun onDroppedFrames(count: Int, elapsedMs: Long) {}
|
||||
override fun onVideoDisabled(counters: DecoderCounters) {
|
||||
Log.v(TAG, "onVideoDisabled()...")
|
||||
}
|
||||
|
||||
// touch event on video player
|
||||
private var touchListener = OnTouchListener { _, event ->
|
||||
//v.performClick() // causes flicker but should be implemented for accessibility
|
||||
mDetector!!.onTouchEvent(event)
|
||||
}
|
||||
|
||||
internal inner class MyGestureListener : SimpleOnGestureListener() {
|
||||
|
||||
/*
|
||||
@Override
|
||||
public boolean onDown(MotionEvent event) {
|
||||
Log.d("TAG","onDown: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
Log.i("TAG", "onSingleTapConfirmed: ");
|
||||
pauseToggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
Log.i("TAG", "onLongPress: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
Log.i("TAG", "onDoubleTap: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2,
|
||||
float distanceX, float distanceY) {
|
||||
Log.i("TAG", "onScroll: ");
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
@RequiresApi(api = VERSION_CODES.N)
|
||||
override fun onFling(
|
||||
event1: MotionEvent, event2: MotionEvent,
|
||||
velocityX: Float, velocityY: Float
|
||||
): Boolean {
|
||||
Log.d(TAG, event1.toString())
|
||||
Log.d(TAG, event2.toString())
|
||||
Log.d(TAG, velocityX.toString())
|
||||
Log.d(TAG, velocityY.toString())
|
||||
//arbitrarily velocity speeds that seem to work to differentiate events.
|
||||
if (velocityY > 4000) {
|
||||
Log.d(TAG, "we have a drag down event")
|
||||
if (VideoHelper.canEnterPipMode(context)) {
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
|
||||
val pipParams = PictureInPictureParams.Builder()
|
||||
requireActivity().enterPictureInPictureMode(pipParams.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (velocityX > 2000 && abs(velocityY) < 2000) {
|
||||
Log.d(TAG, "swipe right $velocityY")
|
||||
}
|
||||
if (velocityX < 2000 && abs(velocityY) < 2000) {
|
||||
Log.d(TAG, "swipe left $velocityY")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "VideoPlayerFragment"
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
/*
|
||||
* 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.intents;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.URLUtil;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
||||
import com.github.se_bastiaan.torrentstream.TorrentOptions;
|
||||
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.model.Video;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
|
||||
public class Intents {
|
||||
|
||||
private static final String TAG = "Intents";
|
||||
|
||||
/**
|
||||
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
|
||||
*
|
||||
* @param context context
|
||||
* @param video video
|
||||
*/
|
||||
// TODO, offer which version to download
|
||||
public static void Share(Context context, Video video) {
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, video.getName());
|
||||
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.getUuid()) );
|
||||
intent.setType("text/plain");
|
||||
|
||||
context.startActivity(intent);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param context context
|
||||
* @param video video
|
||||
*/
|
||||
// TODO, offer which version to download
|
||||
public static void Download(Context context, Video video) {
|
||||
|
||||
if (video.getFiles().size() > 0)
|
||||
{
|
||||
String url = video.getFiles().get( 0 ).getFileDownloadUrl();
|
||||
// make sure it is a valid filename
|
||||
String destFilename = video.getName().replaceAll( "[^a-zA-Z0-9]", "_" ) + "." + MimeTypeMap.getFileExtensionFromUrl( URLUtil.guessFileName( url, null, null ) );
|
||||
|
||||
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
|
||||
DownloadManager.Request request = new DownloadManager.Request( Uri.parse( url ) );
|
||||
request.setDescription( video.getDescription() );
|
||||
request.setTitle( video.getName() );
|
||||
request.allowScanningByMediaScanner();
|
||||
request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
|
||||
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, destFilename );
|
||||
|
||||
// get download service and enqueue file
|
||||
DownloadManager manager = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
|
||||
manager.enqueue( request );
|
||||
} else {
|
||||
Toast.makeText( context, R.string.api_error, Toast.LENGTH_LONG ).show();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,146 @@
|
|||
/*
|
||||
* 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.intents
|
||||
|
||||
import android.Manifest
|
||||
import android.app.DownloadManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.net.Uri
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import android.webkit.MimeTypeMap
|
||||
import android.os.Environment
|
||||
import android.webkit.URLUtil
|
||||
import android.widget.Toast
|
||||
import androidx.core.app.ActivityCompat
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.model.Video
|
||||
import android.content.ContextWrapper
|
||||
|
||||
import android.app.Activity
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
object Intents {
|
||||
private const val TAG = "Intents"
|
||||
|
||||
/**
|
||||
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
|
||||
*
|
||||
* @param context context
|
||||
* @param video video
|
||||
*/
|
||||
// TODO, offer which version to download
|
||||
@JvmStatic
|
||||
fun Share(context: Context, video: Video) {
|
||||
val intent = Intent()
|
||||
intent.action = Intent.ACTION_SEND
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, video.name)
|
||||
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.uuid))
|
||||
intent.type = "text/plain"
|
||||
context.startActivity(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param context context
|
||||
* @param video video
|
||||
*/
|
||||
// TODO, offer which version to download
|
||||
fun Download(context: Context, video: Video) {
|
||||
|
||||
// deal withe permissions here
|
||||
|
||||
// get permission to store file
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) != PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
val activity = getActivity(context)
|
||||
|
||||
if (activity != null) {
|
||||
ActivityCompat.requestPermissions(
|
||||
activity,
|
||||
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
if (ActivityCompat.checkSelfPermission(
|
||||
context,
|
||||
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
) == PackageManager.PERMISSION_GRANTED
|
||||
) {
|
||||
startDownload(video, context)
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(R.string.video_download_permission_error),
|
||||
Toast.LENGTH_LONG
|
||||
).show()
|
||||
}
|
||||
} else {
|
||||
startDownload(video, context)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun startDownload(video: Video, context: Context)
|
||||
{
|
||||
if (video.files.size > 0) {
|
||||
val url = video.files[0].fileDownloadUrl
|
||||
// make sure it is a valid filename
|
||||
val destFilename = video.name.replace(
|
||||
"[^a-zA-Z0-9]".toRegex(),
|
||||
"_"
|
||||
) + "." + MimeTypeMap.getFileExtensionFromUrl(
|
||||
URLUtil.guessFileName(url, null, null)
|
||||
)
|
||||
|
||||
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
|
||||
val request = DownloadManager.Request(Uri.parse(url))
|
||||
request.setDescription(video.description)
|
||||
request.setTitle(video.name)
|
||||
request.allowScanningByMediaScanner()
|
||||
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
|
||||
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, destFilename)
|
||||
|
||||
// get download service and enqueue file
|
||||
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
|
||||
manager.enqueue(request)
|
||||
} else {
|
||||
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun getActivity(context: Context?): Activity? {
|
||||
if (context == null) {
|
||||
return null
|
||||
} else if (context is ContextWrapper) {
|
||||
return if (context is Activity) {
|
||||
context
|
||||
} else {
|
||||
getActivity(context.baseContext)
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import java.util.*
|
||||
|
||||
|
||||
class Comment(
|
||||
|
||||
val id: Int,
|
||||
val url: String,
|
||||
val text: String,
|
||||
val threadId: Int,
|
||||
val inReplyToCommentId: Int? = null,
|
||||
val videoId: Int,
|
||||
val createdAt: Date,
|
||||
val updatedAt: Date,
|
||||
val deletedAt: Date? = null,
|
||||
val isDeleted: Boolean,
|
||||
val totalRepliesFromVideoAuthor: Int,
|
||||
val totalReplies: Int,
|
||||
val account: Account
|
||||
|
||||
)
|
|
@ -0,0 +1,29 @@
|
|||
/*
|
||||
* 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.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
|
||||
import java.util.ArrayList
|
||||
|
||||
class CommentThread(
|
||||
val total: Int,
|
||||
val totalNotDeletedComments: Int,
|
||||
@SerializedName("data")
|
||||
val comments: ArrayList<Comment>
|
||||
|
||||
): OverviewRecycleViewItem()
|
|
@ -35,7 +35,7 @@ class Video(
|
|||
var licence: Licence,
|
||||
var language: Language,
|
||||
var nsfw: Boolean,
|
||||
var description: String,
|
||||
var description: String? = null,
|
||||
var local: Boolean,
|
||||
var live: Boolean,
|
||||
var duration: Int,
|
||||
|
@ -66,7 +66,7 @@ class Video(
|
|||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun getMediaDescription(context: Context?, video: Video): MediaDescriptionCompat {
|
||||
fun getMediaDescription(video: Video): MediaDescriptionCompat {
|
||||
|
||||
// String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
|
||||
|
||||
|
|
|
@ -0,0 +1,7 @@
|
|||
package net.schueller.peertube.model.ui
|
||||
|
||||
import net.schueller.peertube.model.Video
|
||||
|
||||
class VideoMetaViewItem: OverviewRecycleViewItem() {
|
||||
var video: Video? = null
|
||||
}
|
|
@ -16,15 +16,23 @@
|
|||
*/
|
||||
package net.schueller.peertube.network;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import net.schueller.peertube.model.Account;
|
||||
import net.schueller.peertube.model.Channel;
|
||||
import net.schueller.peertube.model.ChannelList;
|
||||
import net.schueller.peertube.model.Me;
|
||||
import net.schueller.peertube.model.VideoList;
|
||||
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
|
@ -52,5 +60,18 @@ public interface GetUserService {
|
|||
@Path(value = "displayName", encoded = true) String displayName
|
||||
);
|
||||
|
||||
@GET("users/me/subscriptions/exist")
|
||||
Call<JsonObject> subscriptionsExist(
|
||||
@Query("uris") String videoChannelNameAndHost
|
||||
);
|
||||
|
||||
@POST("users/me/subscriptions")
|
||||
Call<ResponseBody> subscribe(
|
||||
@Body RequestBody params
|
||||
);
|
||||
|
||||
@DELETE("users/me/subscriptions/{videoChannelNameAndHost}")
|
||||
Call<ResponseBody> unsubscribe(
|
||||
@Path(value = "videoChannelNameAndHost", encoded = true) String videoChannelNameAndHost
|
||||
);
|
||||
}
|
|
@ -16,6 +16,7 @@
|
|||
*/
|
||||
package net.schueller.peertube.network;
|
||||
|
||||
import net.schueller.peertube.model.CommentThread;
|
||||
import net.schueller.peertube.model.Description;
|
||||
import net.schueller.peertube.model.Overview;
|
||||
import net.schueller.peertube.model.Rating;
|
||||
|
@ -91,4 +92,12 @@ public interface GetVideoDataService {
|
|||
@Query("page") int page
|
||||
);
|
||||
|
||||
// https://troll.tv/api/v1/videos/{id}/comment-threads?start=0&count=10&sort=-createdAt
|
||||
@GET("videos/{id}/comment-threads")
|
||||
Call<CommentThread> getCommentThreads(
|
||||
@Path(value = "id", encoded = true) Integer id,
|
||||
@Query("start") int start,
|
||||
@Query("count") int count,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
}
|
|
@ -1,358 +0,0 @@
|
|||
/*
|
||||
* 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.service;
|
||||
|
||||
import static android.media.session.PlaybackState.ACTION_PAUSE;
|
||||
import static android.media.session.PlaybackState.ACTION_PLAY;
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
|
||||
import static net.schueller.peertube.activity.VideoListActivity.EXTRA_VIDEOID;
|
||||
import static net.schueller.peertube.network.UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.activity.VideoPlayActivity;
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.helper.MetaDataHelper;
|
||||
import net.schueller.peertube.model.Video;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class VideoPlayerService extends Service {
|
||||
|
||||
private static final String TAG = "VideoPlayerService";
|
||||
|
||||
private static final String MEDIA_SESSION_TAG = "peertube_player";
|
||||
|
||||
private final IBinder mBinder = new LocalBinder();
|
||||
|
||||
private static final String PLAYBACK_CHANNEL_ID = "playback_channel";
|
||||
|
||||
private static final Integer PLAYBACK_NOTIFICATION_ID = 1;
|
||||
|
||||
public SimpleExoPlayer player;
|
||||
|
||||
private Video currentVideo;
|
||||
|
||||
private String currentStreamUrl;
|
||||
|
||||
private boolean currentStreamUrlIsHLS;
|
||||
|
||||
private PlayerNotificationManager playerNotificationManager;
|
||||
|
||||
private IntentFilter becomeNoisyIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
|
||||
private BecomingNoisyReceiver myNoisyAudioStreamReceiver = new BecomingNoisyReceiver();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.v(TAG, "onCreate...");
|
||||
|
||||
super.onCreate();
|
||||
|
||||
player = new SimpleExoPlayer.Builder(getApplicationContext())
|
||||
.setTrackSelector(new DefaultTrackSelector(getApplicationContext()))
|
||||
.build();
|
||||
|
||||
// Stop player if audio device changes, e.g. headphones unplugged
|
||||
player.addListener(new Player.EventListener() {
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
|
||||
if (playbackState == ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
|
||||
Log.v(TAG, "ACTION_PLAY: " + playbackState);
|
||||
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter);
|
||||
}
|
||||
|
||||
if (playbackState
|
||||
== ACTION_PLAY) { // this means that play is available, hence the audio is paused or stopped
|
||||
Log.v(TAG, "ACTION_PAUSE: " + playbackState);
|
||||
safeUnregisterReceiver();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
|
||||
public VideoPlayerService getService() {
|
||||
// Return this instance of VideoPlayerService so clients can call public methods
|
||||
return VideoPlayerService.this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
|
||||
Log.v(TAG, "onDestroy...");
|
||||
if (playerNotificationManager != null) {
|
||||
playerNotificationManager.setPlayer(null);
|
||||
}
|
||||
//Was seeing an error when exiting the program about not unregistering the receiver.
|
||||
safeUnregisterReceiver();
|
||||
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void safeUnregisterReceiver()
|
||||
{
|
||||
try {
|
||||
unregisterReceiver(myNoisyAudioStreamReceiver);
|
||||
} catch (Exception e) {
|
||||
Log.e("VideoPlayerService", "attempted to unregister a nonregistered service");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Context context = this;
|
||||
Log.v(TAG, "onStartCommand...");
|
||||
|
||||
if (!URLUtil.isValidUrl(currentStreamUrl)) {
|
||||
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show();
|
||||
return START_NOT_STICKY;
|
||||
} else {
|
||||
playVideo();
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setCurrentVideo(Video video) {
|
||||
Log.v(TAG, "setCurrentVideo...");
|
||||
currentVideo = video;
|
||||
}
|
||||
|
||||
public void setCurrentStreamUrl(String streamUrl, boolean isHLS) {
|
||||
Log.v(TAG, "setCurrentStreamUrl..." + streamUrl);
|
||||
currentStreamUrlIsHLS = isHLS;
|
||||
currentStreamUrl = streamUrl;
|
||||
}
|
||||
|
||||
//Playback speed control
|
||||
public void setPlayBackSpeed(float speed) {
|
||||
Log.v(TAG, "setPlayBackSpeed...");
|
||||
player.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current playback speed of the player.
|
||||
*
|
||||
* @return the current playback speed of the player.
|
||||
*/
|
||||
public float getPlayBackSpeed() {
|
||||
return player.getPlaybackParameters().speed;
|
||||
}
|
||||
|
||||
public void playVideo() {
|
||||
Context context = this;
|
||||
|
||||
// We need a valid URL
|
||||
|
||||
Log.v(TAG, "playVideo...");
|
||||
|
||||
// Produces DataSource instances through which media data is loaded.
|
||||
// DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(),
|
||||
// Util.getUserAgent(getApplicationContext(), "PeerTube"), null);
|
||||
|
||||
OkHttpClient.Builder okhttpClientBuilder;
|
||||
|
||||
if (!APIUrlHelper.useInsecureConnection(this)) {
|
||||
okhttpClientBuilder = new OkHttpClient.Builder();
|
||||
} else {
|
||||
okhttpClientBuilder = getUnsafeOkHttpClientBuilder();
|
||||
}
|
||||
|
||||
// Create a data source factory.
|
||||
DataSource.Factory dataSourceFactory = new OkHttpDataSourceFactory(okhttpClientBuilder.build(), Util.getUserAgent(getApplicationContext(), "PeerTube"));
|
||||
|
||||
// Create a progressive media source pointing to a stream uri.
|
||||
MediaSource mediaSource;
|
||||
if (currentStreamUrlIsHLS) {
|
||||
mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
|
||||
} else {
|
||||
mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
|
||||
}
|
||||
|
||||
// Set the media source to be played.
|
||||
player.setMediaSource(mediaSource);
|
||||
|
||||
// Prepare the player.
|
||||
player.prepare();
|
||||
|
||||
// Auto play
|
||||
player.setPlayWhenReady(true);
|
||||
|
||||
//set playback speed to global default
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
float speed = Float.parseFloat(sharedPref.getString(getString(R.string.pref_video_speed_key), "1.0"));
|
||||
|
||||
this.setPlayBackSpeed(speed);
|
||||
|
||||
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
|
||||
context, PLAYBACK_CHANNEL_ID, R.string.playback_channel_name,
|
||||
PLAYBACK_NOTIFICATION_ID,
|
||||
new PlayerNotificationManager.MediaDescriptionAdapter() {
|
||||
|
||||
@Override
|
||||
public String getCurrentContentTitle(Player player) {
|
||||
return currentVideo.getName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PendingIntent createCurrentContentIntent(Player player) {
|
||||
Intent intent = new Intent(context, VideoPlayActivity.class);
|
||||
intent.putExtra(EXTRA_VIDEOID, currentVideo.getUuid());
|
||||
return PendingIntent.getActivity(context, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentContentText(Player player) {
|
||||
return MetaDataHelper.getMetaString(
|
||||
currentVideo.getCreatedAt(),
|
||||
currentVideo.getViews(),
|
||||
getBaseContext()
|
||||
);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bitmap getCurrentLargeIcon(Player player,
|
||||
PlayerNotificationManager.BitmapCallback callback) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
playerNotificationManager.setSmallIcon(R.drawable.ic_logo_bw);
|
||||
|
||||
// don't show skip buttons in notification
|
||||
playerNotificationManager.setUseNavigationActions(false);
|
||||
playerNotificationManager.setUseStopAction(true);
|
||||
|
||||
playerNotificationManager.setNotificationListener(
|
||||
new PlayerNotificationManager.NotificationListener() {
|
||||
@Override
|
||||
public void onNotificationStarted(int notificationId, Notification notification) {
|
||||
startForeground(notificationId, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationCancelled(int notificationId) {
|
||||
Log.v(TAG, "onNotificationCancelled...");
|
||||
stopForeground(true);
|
||||
Intent killFloat = new Intent(ACTION_STOP);
|
||||
sendBroadcast(killFloat);
|
||||
/*
|
||||
Intent killFloat = new Intent(BROADCAST_ACTION);
|
||||
Intent killFloatingWindow = new Intent(getApplicationContext(),VideoPlayActivity.class);
|
||||
killFloatingWindow.putExtra("killFloat",true);
|
||||
|
||||
startActivity(killFloatingWindow);
|
||||
// TODO: only kill the notification if we no longer have a bound activity
|
||||
stopForeground(true);
|
||||
*/
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
playerNotificationManager.setPlayer(player);
|
||||
|
||||
// external Media control, Android Wear / Google Home etc.
|
||||
MediaSessionCompat mediaSession = new MediaSessionCompat(context, MEDIA_SESSION_TAG);
|
||||
mediaSession.setActive(true);
|
||||
playerNotificationManager.setMediaSessionToken(mediaSession.getSessionToken());
|
||||
MediaSessionConnector mediaSessionConnector = new MediaSessionConnector(mediaSession);
|
||||
mediaSessionConnector.setQueueNavigator(new TimelineQueueNavigator(mediaSession) {
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
|
||||
return Video.getMediaDescription(context, currentVideo);
|
||||
}
|
||||
});
|
||||
mediaSessionConnector.setPlayer(player);
|
||||
|
||||
// Audio Focus
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.CONTENT_TYPE_MOVIE)
|
||||
.build();
|
||||
player.setAudioAttributes(audioAttributes, true);
|
||||
|
||||
}
|
||||
|
||||
// pause playback on audio output change
|
||||
private class BecomingNoisyReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
|
||||
player.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -0,0 +1,312 @@
|
|||
/*
|
||||
* 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.service
|
||||
|
||||
import android.app.Notification
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
|
||||
import net.schueller.peertube.model.Video.Companion.getMediaDescription
|
||||
import android.os.IBinder
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||
import android.media.session.PlaybackState
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.network.UnsafeOkHttpClient
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager.MediaDescriptionAdapter
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import net.schueller.peertube.activity.VideoPlayActivity
|
||||
import net.schueller.peertube.activity.VideoListActivity
|
||||
import android.graphics.Bitmap
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.util.Log
|
||||
import android.webkit.URLUtil
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager.NotificationListener
|
||||
import net.schueller.peertube.R.drawable
|
||||
import net.schueller.peertube.R.string
|
||||
import net.schueller.peertube.model.Video
|
||||
import java.lang.Exception
|
||||
|
||||
class VideoPlayerService : Service() {
|
||||
|
||||
private val mBinder: IBinder = LocalBinder()
|
||||
@JvmField
|
||||
var player: ExoPlayer? = null
|
||||
private var currentVideo: Video? = null
|
||||
private var currentStreamUrl: String? = null
|
||||
private var currentStreamUrlIsHLS = false
|
||||
private var playerNotificationManager: PlayerNotificationManager? = null
|
||||
private val becomeNoisyIntentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
||||
private val myNoisyAudioStreamReceiver = BecomingNoisyReceiver()
|
||||
override fun onCreate() {
|
||||
Log.v(TAG, "onCreate...")
|
||||
super.onCreate()
|
||||
player = ExoPlayer.Builder(applicationContext)
|
||||
.setTrackSelector(DefaultTrackSelector(applicationContext))
|
||||
.build()
|
||||
|
||||
// Stop player if audio device changes, e.g. headphones unplugged
|
||||
player!!.addListener(object : Player.Listener {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
if (playbackState.toLong() == PlaybackState.ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
|
||||
Log.v(TAG, "ACTION_PLAY: $playbackState")
|
||||
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter)
|
||||
}
|
||||
if (playbackState
|
||||
.toLong() == PlaybackState.ACTION_PLAY
|
||||
) { // this means that play is available, hence the audio is paused or stopped
|
||||
Log.v(TAG, "ACTION_PAUSE: $playbackState")
|
||||
safeUnregisterReceiver()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
|
||||
// Return this instance of VideoPlayerService so clients can call public methods
|
||||
val service: VideoPlayerService
|
||||
get() =// Return this instance of VideoPlayerService so clients can call public methods
|
||||
this@VideoPlayerService
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.v(TAG, "onDestroy...")
|
||||
if (playerNotificationManager != null) {
|
||||
playerNotificationManager!!.setPlayer(null)
|
||||
}
|
||||
//Was seeing an error when exiting the program about not unregistering the receiver.
|
||||
safeUnregisterReceiver()
|
||||
if (player != null) {
|
||||
player!!.release()
|
||||
player = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun safeUnregisterReceiver() {
|
||||
try {
|
||||
unregisterReceiver(myNoisyAudioStreamReceiver)
|
||||
} catch (e: Exception) {
|
||||
Log.e("VideoPlayerService", "attempted to unregister a non-registered service")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return mBinder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
val context: Context = this
|
||||
Log.v(TAG, "onStartCommand...")
|
||||
return if (!URLUtil.isValidUrl(currentStreamUrl)) {
|
||||
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show()
|
||||
START_NOT_STICKY
|
||||
} else {
|
||||
playVideo()
|
||||
START_STICKY
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentVideo(video: Video?) {
|
||||
Log.v(TAG, "setCurrentVideo...")
|
||||
currentVideo = video
|
||||
}
|
||||
|
||||
fun setCurrentStreamUrl(streamUrl: String, isHLS: Boolean) {
|
||||
Log.v(TAG, "setCurrentStreamUrl...$streamUrl")
|
||||
currentStreamUrlIsHLS = isHLS
|
||||
currentStreamUrl = streamUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current playback speed of the player.
|
||||
*
|
||||
* @return the current playback speed of the player.
|
||||
*///Playback speed control
|
||||
var playBackSpeed: Float
|
||||
get() = player!!.playbackParameters.speed
|
||||
set(speed) {
|
||||
Log.v(TAG, "setPlayBackSpeed...")
|
||||
player!!.playbackParameters = PlaybackParameters(speed)
|
||||
}
|
||||
|
||||
private fun playVideo() {
|
||||
val context: Context = this
|
||||
|
||||
// We need a valid URL
|
||||
Log.v(TAG, "playVideo...")
|
||||
|
||||
// Produces DataSource instances through which media data is loaded.
|
||||
val okhttpClientBuilder: okhttp3.OkHttpClient.Builder = if (!APIUrlHelper.useInsecureConnection(this)) {
|
||||
okhttp3.OkHttpClient.Builder()
|
||||
} else {
|
||||
UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder()
|
||||
}
|
||||
|
||||
// Create a data source factory.
|
||||
val dataSourceFactory: OkHttpDataSource.Factory = OkHttpDataSource.Factory(
|
||||
okhttpClientBuilder.build()
|
||||
)
|
||||
|
||||
// Create a progressive media source pointing to a stream uri.
|
||||
val mediaSource: MediaSource = if (currentStreamUrlIsHLS) {
|
||||
HlsMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
|
||||
} else {
|
||||
ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
|
||||
}
|
||||
|
||||
// Set the media source to be played.
|
||||
player!!.setMediaSource(mediaSource)
|
||||
|
||||
// Prepare the player.
|
||||
player!!.prepare()
|
||||
|
||||
// Auto play
|
||||
player!!.playWhenReady = true
|
||||
|
||||
//set playback speed to global default
|
||||
val sharedPref = getSharedPreferences(
|
||||
packageName + "_preferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
val speed = sharedPref.getString(getString(string.pref_video_speed_key), "1.0")!!.toFloat()
|
||||
playBackSpeed = speed
|
||||
|
||||
playerNotificationManager = PlayerNotificationManager.Builder(
|
||||
this,
|
||||
PLAYBACK_NOTIFICATION_ID,
|
||||
PLAYBACK_CHANNEL_ID,
|
||||
).setMediaDescriptionAdapter(
|
||||
object : MediaDescriptionAdapter {
|
||||
override fun getCurrentContentTitle(player: Player): CharSequence {
|
||||
return currentVideo!!.name
|
||||
}
|
||||
|
||||
override fun createCurrentContentIntent(player: Player): PendingIntent? {
|
||||
val intent = Intent(context, VideoPlayActivity::class.java)
|
||||
intent.putExtra(VideoListActivity.EXTRA_VIDEOID, currentVideo!!.uuid)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
}
|
||||
|
||||
override fun getCurrentContentText(player: Player): CharSequence {
|
||||
return getMetaString(
|
||||
currentVideo!!.createdAt,
|
||||
currentVideo!!.views,
|
||||
baseContext
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCurrentSubText(player: Player): CharSequence { return ""}
|
||||
|
||||
override fun getCurrentLargeIcon(
|
||||
player: Player,
|
||||
callback: PlayerNotificationManager.BitmapCallback
|
||||
): Bitmap? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
).setNotificationListener(
|
||||
object : NotificationListener {
|
||||
|
||||
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
|
||||
super.onNotificationPosted(notificationId, notification, ongoing)
|
||||
if (ongoing) // allow notification to be dismissed if player is stopped
|
||||
startForeground(notificationId, notification)
|
||||
else
|
||||
stopForeground(false)
|
||||
}
|
||||
|
||||
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
|
||||
super.onNotificationCancelled(notificationId, dismissedByUser)
|
||||
stopSelf()
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
).setChannelNameResourceId(string.playback_channel_name)
|
||||
.setChannelDescriptionResourceId(string.playback_channel_description)
|
||||
.build()
|
||||
|
||||
playerNotificationManager!!.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
|
||||
playerNotificationManager!!.setSmallIcon(drawable.ic_logo_bw)
|
||||
|
||||
// don't show skip buttons in notification
|
||||
playerNotificationManager!!.setUseNextAction(false)
|
||||
playerNotificationManager!!.setUsePreviousAction(false)
|
||||
|
||||
playerNotificationManager!!.setPlayer(player)
|
||||
|
||||
// external Media control, Android Wear / Google Home etc.
|
||||
val mediaSession = MediaSessionCompat(context, MEDIA_SESSION_TAG)
|
||||
mediaSession.isActive = true
|
||||
playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken)
|
||||
val mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
|
||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||
return getMediaDescription(currentVideo!!)
|
||||
}
|
||||
})
|
||||
mediaSessionConnector.setPlayer(player)
|
||||
|
||||
// Audio Focus
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
player!!.setAudioAttributes(audioAttributes, true)
|
||||
}
|
||||
|
||||
// pause playback on audio output change
|
||||
private inner class BecomingNoisyReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
|
||||
player!!.playWhenReady = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "VideoPlayerService"
|
||||
private const val MEDIA_SESSION_TAG = "peertube_player"
|
||||
private const val PLAYBACK_CHANNEL_ID = "playback_channel"
|
||||
private const val PLAYBACK_NOTIFICATION_ID = 1
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M6,9l6,6l6,-6"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M18,6L6,18"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M6,6L18,18"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,27 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M21,15v4a2,2 0,0 1,-2 2H5a2,2 0,0 1,-2 -2v-4"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M7,10l5,5l5,-5"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M12,15L12,3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="52dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M13,19l9,-7l-9,-7l0,14z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M2,19l9,-7l-9,-7l0,14z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M4,15s1,-1 4,-1 5,2 8,2 4,-1 4,-1V3s-1,1 -4,1 -5,-2 -8,-2 -4,1 -4,1z"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M4,22L4,15"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,10 @@
|
|||
<vector android:height="52dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000" android:pathData="M6,4h4v16h-4z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M14,4h4v16h-4z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
|
@ -0,0 +1,7 @@
|
|||
<vector android:height="52dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M5,3l14,9l-14,9l0,-18z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
|
@ -0,0 +1,5 @@
|
|||
<vector android:height="24dp" android:viewportHeight="426.7"
|
||||
android:viewportWidth="426.7" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#ffffff" android:pathData="M0,64h256v42.7H0zM0,149.3h256V192H0zM0,234.7h170.7v42.7H0z"/>
|
||||
<path android:fillColor="#ffffff" android:pathData="M341.3,234.7v-85.4h-42.6v85.4h-85.4v42.6h85.4v85.4h42.6v-85.4h85.4v-42.6z"/>
|
||||
</vector>
|
|
@ -0,0 +1,11 @@
|
|||
<vector android:height="52dp" android:viewportHeight="24"
|
||||
android:viewportWidth="24" android:width="52dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M11,19l-9,-7l9,-7l0,14z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
<path android:fillColor="#00000000"
|
||||
android:pathData="M22,19l-9,-7l9,-7l0,14z"
|
||||
android:strokeColor="#ffffff" android:strokeLineCap="round"
|
||||
android:strokeLineJoin="round" android:strokeWidth="2"/>
|
||||
</vector>
|
|
@ -0,0 +1,41 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M18,5m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M6,12m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M18,19m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M8.59,13.51L15.42,17.49"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M15.41,6.51L8.59,10.49"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,20 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M12,12m-10,0a10,10 0,1 1,20 0a10,10 0,1 1,-20 0"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
<path
|
||||
android:pathData="M4.93,4.93L19.07,19.07"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10,15v4a3,3 0,0 0,3 3l4,-9L17,2L5.72,2a2,2 0,0 0,-2 1.7l-1.38,9a2,2 0,0 0,2 2.3zM17,2h2.67A2.31,2.31 0,0 1,22 4v7a2.31,2.31 0,0 1,-2.33 2L17,13"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M10,15v4a3,3 0,0 0,3 3l4,-9L17,2L5.72,2a2,2 0,0 0,-2 1.7l-1.38,9a2,2 0,0 0,2 2.3zM17,2h2.67A2.31,2.31 0,0 1,22 4v7a2.31,2.31 0,0 1,-2.33 2L17,13"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#555555"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14,9V5a3,3 0,0 0,-3 -3l-4,9v11h11.28a2,2 0,0 0,2 -1.7l1.38,-9a2,2 0,0 0,-2 -2.3zM7,22H4a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2h3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#00000000"
|
||||
android:strokeColor="#ffffff"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -0,0 +1,13 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:pathData="M14,9V5a3,3 0,0 0,-3 -3l-4,9v11h11.28a2,2 0,0 0,2 -1.7l1.38,-9a2,2 0,0 0,-2 -2.3zM7,22H4a2,2 0,0 1,-2 -2v-7a2,2 0,0 1,2 -2h3"
|
||||
android:strokeLineJoin="round"
|
||||
android:strokeWidth="2"
|
||||
android:fillColor="#ffffff"
|
||||
android:strokeColor="#555555"
|
||||
android:strokeLineCap="round"/>
|
||||
</vector>
|
|
@ -6,36 +6,31 @@
|
|||
android:keepScreenOn="true"
|
||||
tools:context="net.schueller.peertube.activity.VideoPlayActivity">
|
||||
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
<RelativeLayout
|
||||
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<fragment android:name="net.schueller.peertube.fragment.VideoPlayerFragment"
|
||||
<fragment
|
||||
android:id="@+id/video_player_fragment"
|
||||
android:name="net.schueller.peertube.fragment.VideoPlayerFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="250dp" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/video_player_fragment"
|
||||
android:layout_marginTop="250dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_below="@id/video_player_fragment"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ScrollView
|
||||
android:id="@+id/login_form"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<fragment android:name="net.schueller.peertube.fragment.VideoMetaDataFragment"
|
||||
android:id="@+id/video_meta_data_fragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content" />
|
||||
|
||||
</ScrollView>
|
||||
</RelativeLayout>
|
||||
|
||||
<fragment
|
||||
android:id="@+id/video_meta_data_fragment"
|
||||
android:name="net.schueller.peertube.fragment.VideoMetaDataFragment"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
/>
|
||||
|
||||
</RelativeLayout>
|
||||
</androidx.coordinatorlayout.widget.CoordinatorLayout>
|
||||
|
|
|
@ -0,0 +1,225 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<LinearLayout android:layout_width="match_parent"
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:orientation="vertical"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:background="?android:colorBackground"
|
||||
android:clickable="true"
|
||||
android:focusable="true"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="12dp"
|
||||
android:paddingBottom="12dp">
|
||||
|
||||
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:gravity="start"
|
||||
android:text="@string/video_meta_title_description"
|
||||
android:textSize="24sp" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_description_close_button"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:background="@android:color/transparent"
|
||||
android:gravity="end"
|
||||
android:src="@drawable/ic_close"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<ScrollView
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="12dp">
|
||||
|
||||
<TextView
|
||||
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:autoLink="web"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/description"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_privacy"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_privacy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_category"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_category"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_license"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_license"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_language"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_tags"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_tags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random" />
|
||||
|
||||
</LinearLayout>
|
||||
</LinearLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
</ScrollView>
|
||||
</LinearLayout>
|
|
@ -1,364 +1,15 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp">
|
||||
android:id="@+id/videoMetaFragment"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:contentDescription="@string/video_row_account_avatar"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="12dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sl_row_name"
|
||||
<!-- Related Videos -->
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/relatedVideosView"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentTop="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_toEndOf="@+id/avatar"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
|
||||
android:layout_height="match_parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoMeta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/sl_row_name"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:layout_toEndOf="@+id/avatar"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoOwner"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/videoMeta"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_toEndOf="@id/avatar"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/moreButton"
|
||||
android:layout_width="45dp"
|
||||
android:layout_height="45dp"
|
||||
android:layout_marginStart="-16dp"
|
||||
android:layout_marginTop="16dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:layout_toEndOf="@+id/sl_row_name"
|
||||
android:background="@null"
|
||||
android:contentDescription="@string/descr_overflow_button"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/video_actions"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/videoOwner"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="65dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/video_thumbs_up"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_thumbs_up_total"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="65dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/video_thumbs_down"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_thumbs_down_total"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="65dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/video_share"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_share_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_share"
|
||||
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="65dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<Button
|
||||
android:id="@+id/video_download"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="48dp"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_download_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_download"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/video_actions"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:autoLink="web"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/description"
|
||||
android:layout_marginStart="18dp"
|
||||
android:layout_marginTop="12dp"
|
||||
android:layout_marginEnd="12dp"
|
||||
android:orientation="vertical">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_privacy"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_privacy"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
android:lines="2"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_category"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_category"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
android:lines="2"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_license"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_license"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem"
|
||||
android:lines="2"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_language"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_language"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
android:lines="2"
|
||||
/>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="12dp"
|
||||
android:gravity="bottom"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_tags"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<Space
|
||||
android:layout_width="12dp"
|
||||
android:layout_height="1dp" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_tags"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center|start"
|
||||
tools:text="@tools:sample/lorem/random"
|
||||
android:lines="2"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
</LinearLayout>
|
||||
</androidx.recyclerview.widget.RecyclerView>
|
||||
|
||||
</RelativeLayout>
|
|
@ -0,0 +1,94 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:paddingTop="6dp"
|
||||
android:padding="6dp"
|
||||
android:id="@+id/video_title_block"
|
||||
>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/video_comments_title_wrapper"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_comments_title"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:text="@string/video_comments_title"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_comments_total_count"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerInParent="true"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:layout_toEndOf="@+id/video_comments_title"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/video_meta_show_description"
|
||||
android:src="@drawable/ic_chevron_down"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/video_highlighted_comment_wrapper"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@id/video_comments_title_wrapper">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/video_highlighted_avatar"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_marginStart="8dp"
|
||||
android:layout_marginTop="8dp"
|
||||
android:layout_marginBottom="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintHorizontal_bias="0.0"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_highlighted_comment"
|
||||
android:layout_toEndOf="@+id/video_highlighted_avatar"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="6dp"
|
||||
android:textSize="12sp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -0,0 +1,402 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout
|
||||
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="wrap_content">
|
||||
|
||||
<RelativeLayout
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent">
|
||||
|
||||
<!-- Video Title Block -->
|
||||
<RelativeLayout
|
||||
android:id="@+id/video_title_block"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:padding="6dp"
|
||||
android:paddingTop="6dp">
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/video_open_description"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginEnd="6dp">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_name"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="0dp"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="24dp"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Title" />
|
||||
|
||||
<ImageButton
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:background="@android:color/transparent"
|
||||
android:clickable="false"
|
||||
android:contentDescription="@string/video_meta_show_description"
|
||||
android:src="@drawable/ic_chevron_down"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoMeta"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/video_open_description"
|
||||
android:layout_alignParentEnd="true"
|
||||
android:layout_marginStart="6dp"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="6dp"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<!-- video actions -->
|
||||
<HorizontalScrollView
|
||||
android:id="@+id/video_actions_block"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/video_title_block"
|
||||
android:paddingBottom="6dp"
|
||||
android:scrollbars="none">
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/video_actions"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="6dp"
|
||||
android:layout_marginEnd="18dp"
|
||||
android:paddingEnd="18dp"
|
||||
android:paddingStart="18dp"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_thumbs_up_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_thumbs_up"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_thumbs_up"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_thumbs_up_total"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_thumbs_down_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_thumbs_down"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_thumbs_down"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_thumbs_down_total"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_share_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_share"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_share_2"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_share_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_share"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_download_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_download"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_download"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_download_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_meta_button_download"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_add_to_playlist_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_add_to_playlist"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_playlist_add"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/video_add_to_playlist_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_add_to_playlist"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_block_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_block"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_slash"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vvideo_block_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_block"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="1dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="70dp"
|
||||
android:id="@+id/video_flag_wrapper"
|
||||
android:layout_height="wrap_content"
|
||||
android:orientation="vertical">
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/video_flag"
|
||||
android:layout_width="42dp"
|
||||
android:layout_height="42dp"
|
||||
android:layout_gravity="center"
|
||||
android:background="?android:selectableItemBackground"
|
||||
android:gravity="center"
|
||||
android:clickable="false"
|
||||
android:src="@drawable/ic_flag"
|
||||
app:tint="?attr/colorPrimary" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/vvideo_flag_text"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:gravity="center"
|
||||
android:text="@string/video_flag"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button"
|
||||
android:textSize="12sp" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
</HorizontalScrollView>
|
||||
|
||||
<TextView
|
||||
android:layout_below="@+id/video_actions_block"
|
||||
android:id="@+id/video_action_block_line"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?android:colorEdgeEffect"
|
||||
android:height="1dp"
|
||||
android:gravity="center_horizontal"/>
|
||||
|
||||
<RelativeLayout
|
||||
android:id="@+id/video_account_block"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_below="@+id/video_action_block_line"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="6dp"
|
||||
android:paddingEnd="6dp">
|
||||
|
||||
<de.hdodenhof.circleimageview.CircleImageView
|
||||
android:id="@+id/avatar"
|
||||
android:layout_width="72dp"
|
||||
android:layout_height="72dp"
|
||||
android:layout_alignParentStart="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:contentDescription="@string/video_row_account_avatar"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingTop="12dp"
|
||||
android:paddingEnd="12dp" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginStart="12dp"
|
||||
android:layout_toEndOf="@+id/avatar"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoOwner"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoOwnerSubscribers"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="3dp"
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/videoOwnerSubscribeButton"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_alignParentRight="true"
|
||||
android:layout_centerVertical="true"
|
||||
android:layout_marginTop="0dp"
|
||||
android:layout_marginEnd="0dp"
|
||||
android:gravity="end"
|
||||
android:text=""
|
||||
android:textAppearance="@style/Base.TextAppearance.AppCompat.Button" />
|
||||
|
||||
|
||||
<!-- <TextView-->
|
||||
<!-- android:id="@+id/moreButton"-->
|
||||
<!-- android:layout_width="45dp"-->
|
||||
<!-- android:layout_height="45dp"-->
|
||||
<!-- android:layout_marginStart="-16dp"-->
|
||||
<!-- android:layout_marginTop="16dp"-->
|
||||
<!-- android:layout_marginEnd="0dp"-->
|
||||
<!-- android:layout_toEndOf="@+id/sl_row_name"-->
|
||||
<!-- android:background="@null"-->
|
||||
<!-- android:contentDescription="@string/descr_overflow_button"-->
|
||||
<!-- android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />-->
|
||||
|
||||
</RelativeLayout>
|
||||
|
||||
<TextView
|
||||
android:layout_below="@+id/video_account_block"
|
||||
android:id="@+id/video_account_block_line"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_width="match_parent"
|
||||
android:background="?android:colorEdgeEffect"
|
||||
android:height="1dp"
|
||||
android:gravity="center_horizontal"/>
|
||||
</RelativeLayout>
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -8,7 +8,7 @@
|
|||
android:background="#CC000000"
|
||||
android:layoutDirection="ltr"
|
||||
android:orientation="vertical"
|
||||
tools:targetApi="28">
|
||||
tools:targetApi="32">
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/exo_more_button"
|
||||
|
@ -18,14 +18,14 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/exo_more"
|
||||
android:layout_width="18dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="end"
|
||||
android:paddingTop="12dp"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:textColor="#FFBEBEBE"
|
||||
android:textSize="12sp" />
|
||||
android:textSize="18sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -43,27 +43,63 @@
|
|||
android:gravity="center"
|
||||
android:orientation="horizontal"
|
||||
android:paddingTop="8dp">
|
||||
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
<ImageButton
|
||||
android:id="@id/exo_rew"
|
||||
style="@style/ExoMediaButton.Rewind" />
|
||||
|
||||
android:layout_width="72sp"
|
||||
android:layout_height="52sp"
|
||||
android:layout_gravity="start"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/exo_controls_rewind_description"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_rewind" />
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
<ImageButton
|
||||
android:id="@id/exo_repeat_toggle"
|
||||
style="@style/ExoMediaButton" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_play"
|
||||
style="@style/ExoMediaButton.Play" />
|
||||
android:contentDescription="@string/exo_controls_play_description"
|
||||
android:src="@drawable/ic_play"
|
||||
android:layout_height="52sp"
|
||||
android:layout_width="72sp"
|
||||
android:scaleType="center"
|
||||
android:background="@android:color/transparent"
|
||||
/>
|
||||
|
||||
<ImageButton
|
||||
android:id="@id/exo_pause"
|
||||
style="@style/ExoMediaButton.Pause" />
|
||||
|
||||
android:contentDescription="@string/exo_controls_pause_description"
|
||||
android:src="@drawable/ic_pause"
|
||||
android:layout_height="52sp"
|
||||
android:layout_width="72sp"
|
||||
android:scaleType="center"
|
||||
android:background="@android:color/transparent"
|
||||
/>
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
<ImageButton
|
||||
android:id="@id/exo_ffwd"
|
||||
style="@style/ExoMediaButton.FastForward" />
|
||||
|
||||
android:layout_width="72sp"
|
||||
android:layout_height="52sp"
|
||||
android:layout_gravity="end"
|
||||
android:background="@android:color/transparent"
|
||||
android:contentDescription="@string/exo_controls_fastforward_description"
|
||||
android:scaleType="center"
|
||||
android:src="@drawable/ic_fast_forward" />
|
||||
<Space
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
</LinearLayout>
|
||||
|
||||
|
||||
|
@ -86,9 +122,29 @@
|
|||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_gravity="center"
|
||||
android:paddingLeft="12dp"
|
||||
android:paddingRight="12dp"
|
||||
android:textColor="#FFBEBEBE"
|
||||
android:paddingStart="12dp"
|
||||
android:paddingEnd="2dp"
|
||||
android:textColor="#FFFFFF"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<TextView
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:textColor="#BABABA"
|
||||
android:textSize="14sp"
|
||||
android:includeFontPadding="false"
|
||||
android:text="@string/player_time_seperator" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:includeFontPadding="false"
|
||||
android:paddingStart="2dp"
|
||||
android:paddingEnd="6dp"
|
||||
android:textColor="#BABABA"
|
||||
android:textSize="14sp" />
|
||||
|
||||
<View
|
||||
|
@ -96,18 +152,6 @@
|
|||
android:layout_height="0dp"
|
||||
android:layout_weight="1" />
|
||||
|
||||
<TextView
|
||||
android:id="@id/exo_duration"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:includeFontPadding="false"
|
||||
android:layout_gravity="center"
|
||||
android:paddingLeft="6dp"
|
||||
android:paddingRight="6dp"
|
||||
android:textColor="#FFBEBEBE"
|
||||
android:textSize="14sp" />
|
||||
|
||||
|
||||
|
||||
<FrameLayout
|
||||
android:id="@+id/exo_fullscreen_button"
|
||||
|
@ -117,13 +161,13 @@
|
|||
|
||||
<TextView
|
||||
android:id="@+id/exo_fullscreen"
|
||||
android:layout_width="18dp"
|
||||
android:layout_height="18dp"
|
||||
android:layout_width="24dp"
|
||||
android:layout_height="24dp"
|
||||
android:layout_gravity="center"
|
||||
android:adjustViewBounds="true"
|
||||
android:scaleType="fitCenter"
|
||||
android:textColor="#FFBEBEBE"
|
||||
android:textSize="12sp" />
|
||||
android:textSize="18sp" />
|
||||
|
||||
</FrameLayout>
|
||||
|
||||
|
@ -146,20 +190,20 @@
|
|||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:visibility="gone"
|
||||
android:id="@+id/exo_torrent_status"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content">
|
||||
<!-- <LinearLayout-->
|
||||
<!-- android:visibility="gone"-->
|
||||
<!-- android:id="@+id/exo_torrent_status"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content">-->
|
||||
|
||||
<ProgressBar
|
||||
android:id="@+id/torrent_progress"
|
||||
style="?android:attr/progressBarStyleHorizontal"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:indeterminate="false"
|
||||
android:max="100" />
|
||||
<!-- <ProgressBar-->
|
||||
<!-- android:id="@+id/torrent_progress"-->
|
||||
<!-- style="?android:attr/progressBarStyleHorizontal"-->
|
||||
<!-- android:layout_width="match_parent"-->
|
||||
<!-- android:layout_height="wrap_content"-->
|
||||
<!-- android:indeterminate="false"-->
|
||||
<!-- android:max="100" />-->
|
||||
|
||||
</LinearLayout>
|
||||
<!-- </LinearLayout>-->
|
||||
|
||||
</LinearLayout>
|
|
@ -64,6 +64,7 @@
|
|||
<string name="title_activity_video_play" translatable="false">VideoPlayActivity</string>
|
||||
|
||||
<string name="playback_channel_name" translatable="false">PeerTube</string>
|
||||
<string name="playback_channel_description" translatable="false">playback_channel</string>
|
||||
|
||||
<string name="peertube_instance_search_default_description" translatable="false">PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.</string>
|
||||
|
||||
|
|
|
@ -366,4 +366,14 @@
|
|||
<string name="video_list_live_marker">LIVE</string>
|
||||
<string name="video_get_full_description_failed">Getting full video description failed</string>
|
||||
<string name="video_description_read_more">Read More</string>
|
||||
<string name="player_time_seperator">/</string>
|
||||
<string name="video_meta_show_description">Show Description</string>
|
||||
<string name="video_meta_title_description">Description</string>
|
||||
<string name="video_add_to_playlist">Save</string>
|
||||
<string name="video_block">Block</string>
|
||||
<string name="video_flag">Flag</string>
|
||||
<string name="video_feature_not_yet_implemented">This feature has not yet been implemented. Coming soon!</string>
|
||||
<string name="subscribe">Subscribe</string>
|
||||
<string name="unsubscribe">Unsubscribe</string>
|
||||
<string name="video_comments_title">Comments</string>
|
||||
</resources>
|
|
@ -76,12 +76,12 @@
|
|||
app:title="@string/pref_background_behavior"
|
||||
app:iconSpaceReserved="false"/>
|
||||
|
||||
<SwitchPreference
|
||||
app:defaultValue="false"
|
||||
app:key="@string/pref_torrent_player_key"
|
||||
app:summary="@string/pref_description_torrent_player"
|
||||
app:title="@string/pref_title_torrent_player"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<!-- <SwitchPreference-->
|
||||
<!-- app:defaultValue="false"-->
|
||||
<!-- app:key="@string/pref_torrent_player_key"-->
|
||||
<!-- app:summary="@string/pref_description_torrent_player"-->
|
||||
<!-- app:title="@string/pref_title_torrent_player"-->
|
||||
<!-- app:iconSpaceReserved="false"/>-->
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
|
|
@ -2,11 +2,11 @@
|
|||
|
||||
buildscript {
|
||||
|
||||
ext.kotlin_version = '1.5.31'
|
||||
ext.kotlin_version = '1.6.10'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
|
@ -21,7 +21,6 @@ buildscript {
|
|||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'https://oss.sonatype.org/content/repositories/snapshots'
|
||||
|
|
Loading…
Reference in New Issue