Merge branch 'develop' into 'master'

Release

See merge request sschueller/peertube!47
This commit is contained in:
Stefan Schüller 2022-01-01 14:09:28 +00:00
commit b29afe52a2
138 changed files with 4200 additions and 2467 deletions

View File

@ -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

View File

@ -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

View File

@ -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()
}

View File

@ -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
}
}

View File

@ -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");
}
}
}

View File

@ -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
}
}

View File

@ -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}
}
}

View File

@ -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"
}
}

View File

@ -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")

View File

@ -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

View File

@ -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 {

View File

@ -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()
}
}
}

View File

@ -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();
}
}
}

View File

@ -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"
}
}

View File

@ -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;
}
}
}

View File

@ -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"
}
}

View File

@ -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();
}
}
}

View File

@ -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
}
}

View File

@ -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
)

View File

@ -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()

View File

@ -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);

View File

@ -0,0 +1,7 @@
package net.schueller.peertube.model.ui
import net.schueller.peertube.model.Video
class VideoMetaViewItem: OverviewRecycleViewItem() {
var video: Video? = null
}

View File

@ -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
);
}

View File

@ -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
);
}

View File

@ -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);
}
}
}
}

View File

@ -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
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -345,7 +345,7 @@
<string name="pref_title_back_pause">ايقاف عند الضغط على زر العودة</string>
<string name="pref_title_buildtime">تاريخ ووقت البناء</string>
<string name="network_error">خطأ في الوصول للشبكة، تحقق من اتصالك</string>
<string name="server_selection_filter_hint">فلترة القائمة</string>
<string name="server_selection_filter_hint">تنقيح القائمة</string>
<string name="pref_background_behavior">إعدادات التشغيل في الخلفية</string>
<string name="pref_background_stop">إيقاف تشغيل الكل</string>
<string name="pref_background_audio">تابع تشغيل الصوت في الخلفية</string>
@ -361,4 +361,7 @@
<string name="server_book_add_save_button">حفظ</string>
<string name="pref_title_accept_insecure">تعطيل التحقق من شهادة SSL</string>
<string name="pref_description_accept_insecure">تجاهل الاتصالات غير الآمنة. استخدم هذا فقط إذا كنت تعرف الخادم الذي تتصل به. يتطلب إعادة تشغيل التطبيق.</string>
<string name="pref_title_video_speed">سرعة التشغيل الافتراضية</string>
<string name="pref_description_video_speed">حدد سرعة تشغيل الفيديو العامة</string>
<string name="action_bar_title_address_book">دفتر العناوين</string>
</resources>

View File

@ -362,4 +362,7 @@
<string name="settings_activity_advanced_category_title">উন্নত</string>
<string name="server_book_add_save_button">সংরক্ষন</string>
<string name="video_list_live_marker">লাইভ</string>
<string name="action_bar_title_address_book">ঠিকানা বই</string>
<string name="pref_title_video_speed">সহজাত গতি</string>
<string name="pref_description_video_speed">সর্বজনীন গতি নির্ধারণ করো</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="pref_title_accept_insecure">SSL-Zertifikatsprüfung deaktivieren</string>
<string name="pref_description_accept_insecure">Unsichere Verbindungen ignorieren. Verwenden Sie dies nur, wenn Sie den Server kennen, mit dem Sie sich verbinden. Erfordert einen Neustart der Anwendung.</string>
<string name="video_list_live_marker">LIVE</string>
<string name="pref_title_video_speed">Standard-Wiedergabegeschwindigkeit</string>
<string name="pref_description_video_speed">Wählen Sie die globale Videowiedergabegeschwindigkeit</string>
<string name="action_bar_title_address_book">Adressbuch</string>
</resources>

View File

@ -118,13 +118,13 @@
<string name="me_logout_button">Cerrar sesión</string>
<string name="server_book_valid_url_is_required">Una URL válida es requerida</string>
<string name="server_book_label_is_required">La etiqueta de servidor es requerida</string>
<string name="rsl">Ruso (Lenguaje de señas)</string>
<string name="rsl">Lengua de señas rusa</string>
<string name="ru">Ruso</string>
<string name="ro">Romano</string>
<string name="ro">Rumano</string>
<string name="pt">Portugués</string>
<string name="pl">Polaco</string>
<string name="no">Noruego</string>
<string name="pks">Pakistaní (Lenguaje de señas)</string>
<string name="pks">Lengua de señas de Pakistán</string>
<string name="fsl">Francés (Lenguaje de señas)</string>
<string name="fr">Francés</string>
<string name="fi">Finlandés</string>
@ -193,4 +193,111 @@
<string name="pref_description_back_pause">Pausa la reproducción de fondo al presionar atrás durante la reproducción de vídeo.</string>
<string name="video_list_live_marker">EN VIVO</string>
<string name="co">Corso</string>
<string name="nv">Navajo</string>
<string name="sl">Esloveno</string>
<string name="fj">Fiyiano</string>
<string name="cv">Chuvasio</string>
<string name="cr">Cree</string>
<string name="nl">Holandés</string>
<string name="dz">Dzongkha</string>
<string name="et">Estonio</string>
<string name="ee">Ewe</string>
<string name="hi">Hindi</string>
<string name="ho">Hiri Motu</string>
<string name="hu">Húngaro</string>
<string name="is">Islandés</string>
<string name="ga">Irlandés</string>
<string name="it">Italiano</string>
<string name="kr">Kanuri</string>
<string name="mg">Malgache</string>
<string name="ml">Malayalam</string>
<string name="mt">Maltés</string>
<string name="gv">Manés</string>
<string name="mi">Maori</string>
<string name="pa">Panyabí</string>
<string name="fa">Persa</string>
<string name="rm">Romanche</string>
<string name="sr">Serbio</string>
<string name="ta">Tamil</string>
<string name="te">Telugu</string>
<string name="bo">Tibetano</string>
<string name="ti">Tigriña</string>
<string name="ts">Tsonga</string>
<string name="tn">Tswana</string>
<string name="tr">Turco</string>
<string name="tk">Turcomano</string>
<string name="tw">Twi</string>
<string name="ug">Uigur</string>
<string name="mn">Mongol</string>
<string name="lg">Ganda</string>
<string name="xh">Xhosa</string>
<string name="th">Tailandés</string>
<string name="lb">Luxemburgués</string>
<string name="jv">Javanés</string>
<string name="kw">Córnico</string>
<string name="dv">Dhivehi</string>
<string name="fo">Feroés</string>
<string name="ff">Fula</string>
<string name="gl">Gallego</string>
<string name="ka">Georgiano</string>
<string name="de">Alemán</string>
<string name="gsg">Lengua de señas alemana</string>
<string name="gn">Guaraní</string>
<string name="gu">Guyaratí</string>
<string name="ht">Haitiano</string>
<string name="ha">Hausa</string>
<string name="he">Hebreo</string>
<string name="hz">Herero</string>
<string name="ig">Igbo</string>
<string name="id">Indonesio</string>
<string name="iu">Inuktitut</string>
<string name="ik">Inupiaq</string>
<string name="ja">Japonés</string>
<string name="jsl">Lengua de señas japonesa</string>
<string name="kl">Kalaallisut</string>
<string name="kn">Kannada</string>
<string name="ks">Cachemiro</string>
<string name="tlh">Klingon</string>
<string name="ko">Coreano</string>
<string name="ku">Kurdo</string>
<string name="lo">Lao</string>
<string name="lv">Letón</string>
<string name="kk">Kazajo</string>
<string name="km">Jémer</string>
<string name="ki">Kikuyu</string>
<string name="mk">Macedonio</string>
<string name="nb">Noruego bokmål</string>
<string name="nn">Noruego nynorsk</string>
<string name="oc">Occitano</string>
<string name="oj">Ojibwa</string>
<string name="ps">Pastún</string>
<string name="qu">Quechua</string>
<string name="sdl">Lenguaje de señas de Arabia Saudita</string>
<string name="sk">Eslovaco</string>
<string name="so">Somalí</string>
<string name="sfs">Lengua de señas sudafricana</string>
<string name="es">Español</string>
<string name="sv">Sueco</string>
<string name="swl">Lengua de señas sueca</string>
<string name="tl">Tagalo</string>
<string name="ty">Tahitiano</string>
<string name="tg">Tayiko</string>
<string name="to">Tonga (Islas Tonga)</string>
<string name="uk">Ucraniano</string>
<string name="ur">Urdu</string>
<string name="uz">Uzbeko</string>
<string name="ve">Venda</string>
<string name="vi">Vietnamita</string>
<string name="wa">Valón</string>
<string name="cy">Galés</string>
<string name="fy">Frisón occidental</string>
<string name="wo">Wolof</string>
<string name="yi">Yidis</string>
<string name="yo">Yoruba</string>
<string name="zu">Zulú</string>
<string name="pref_title_video_speed">Velocidad de reproducción por defecto</string>
<string name="pref_description_video_speed">Seleccione la velocidad global de reproducción de vídeo</string>
<string name="ms">Malayo (macrolengua)</string>
<string name="gd">Gaélico escocés</string>
<string name="sh">Serbo-croata</string>
</resources>

View File

@ -20,25 +20,25 @@
<string name="bottom_nav_title_local">محلی</string>
<string name="bottom_nav_title_subscriptions">اشتراک‌ها</string>
<string name="bottom_nav_title_account">حساب</string>
<string name="meta_data_views">" بازدیدها"</string>
<string name="video_row_video_thumbnail">تصویر ویدئو</string>
<string name="video_row_account_avatar">آواتار حساب</string>
<string name="search_hint">جستجو در پیرتیوب</string>
<string name="title_activity_search">جستجو</string>
<string name="meta_data_views">" نمایش"</string>
<string name="video_row_video_thumbnail">بندانگشتی ویدیو</string>
<string name="video_row_account_avatar">چهرک حساب</string>
<string name="search_hint">جست‌وجوی پیرتیوب</string>
<string name="title_activity_search">جست‌وجو</string>
<string name="no_data_available">بدون نتیجه</string>
<string name="descr_overflow_button">بیشتر</string>
<string name="descr_overflow_button">بیشتر</string>
<string name="menu_share">هم‌رسانی</string>
<string name="invalid_url">نشانی نامعتبر</string>
<string name="invalid_url">نشانی نامعتبر.</string>
<string name="pref_title_dark_mode">حالت تاریک</string>
<string name="pref_description_dark_mode">برای اعمال حالت تاریک، برنامه را از اول راه‌اندازی کنید.</string>
<string name="pref_title_app_theme">سبک برنامه</string>
<string name="pref_description_app_theme">برای اعمال سبک، برنامه را از اول راه‌اندازی کنید</string>
<string name="pref_title_torrent_player">پخش‌کننده ویدئوی تورنت</string>
<string name="pref_title_app_theme">زمینهٔ کاره</string>
<string name="pref_description_app_theme">برای تأثیر گذاشتن زمینه، کاره را دوباره آغاز کنید.</string>
<string name="pref_title_torrent_player">پخش‌کنندهٔ ویدیوی تورنت</string>
<string name="pref_title_license">پروانه</string>
<string name="pref_title_version">نسخه</string>
<string name="pref_title_show_nsfw">محتوا NSFW</string>
<string name="pref_title_version">نگارش</string>
<string name="pref_title_show_nsfw">محتوای NSFW</string>
<string name="pref_description_show_nsfw">نمایش محتوای NSFW</string>
<string name="pref_language">صافی زبان</string>
<string name="pref_language">پالایهٔ زبان</string>
<string name="pref_title_peertube_server">کارساز پیرتیوب</string>
<string name="pref_title_background_play">پخش در پس‌زمینه</string>
<string name="menu_video_more_report">گزارش</string>
@ -89,7 +89,7 @@
<string name="fr">فرانسوی</string>
<string name="fi">فنلاندی</string>
<string name="en">انگلیسی</string>
<string name="as"/>
<string name="as">آسامی</string>
<string name="da">دانمارکی</string>
<string name="zh">چینی</string>
<string name="bg">بلغاری</string>
@ -98,23 +98,25 @@
<string name="az">آذربایجانی</string>
<string name="hy">ارمنی</string>
<string name="ar">عربی</string>
<string name="pref_description_background_play">در صورت فعال بودن، پخش ویدئو در پس‌زمینه ادامه می‌یابد.</string>
<string name="pref_description_language">به جای نشان دادن همه ویدئه تحت همه زبان‌ها، یک زبان برای ویدئو انتخاب کنید.</string>
<string name="pref_description_background_play">اگر به کار افتاده باشد، پخش ویدیو را در پس‌زمینه ادامه می‌دهد.</string>
<string name="pref_description_language">به جای نمایش تمامی ویدیوها به همهٔ زبان‌ها، زبانی برای ویدیو برگزینید.</string>
<string name="pref_description_license">
\n<b>پروانه عمومی همگانی آفرو نسخه ۳ AGPLv3</b>
\n
\nمجوزهای این پروانه که قوی‌ترین پروانه کپی‌لفت است مشروط به دردسترس قرار دادن کامل کد منبع کارهای تحت پروانه و نسخه‌های تغییریافته‌شان است که شامل کارهای بزرگ‌تری که تحت همین پروانه × از این کار استفاده می‌کنند می‌شود. تذکر پروانه و کپی‌رایت باید محفوظ بماند. مشارکت‌کنندگان باید واگذاری حقوق پتنت را اعلام کنند. وقتی نسخه تغییر یافته برای ارائه خدمت روی شبکه استفاده شود، کد منبع نسخه تغییر یافته بایستی به صورت کامل دردسترس قرار بگیرد.</string>
<string name="pref_description_torrent_player">پخش ویدئو از طریق جریان تورنت. این ویژگی، نیازمند مجور دسترسی به فضای ذخیره‌سازی است. (آلفا، ناپایدار!)</string>
<string name="pref_description_torrent_player">پخش ویدیو با جریان تورنت. این ویژگی، نیازمند اجازهٔ ذخیره‌سازی است. (آلفا، ناپایدار!)</string>
<string name="bottom_nav_title_discover">نوار ناوبری پایین</string>
<string name="settings_api_error_float">نسخه اندروید از ویدئوی شناور پشتیبانی نمی کند</string>
<string name="pref_background_behavior">پیکربندی پخش در پس زمینه</string>
<string name="pref_background_float">پخش ویدیو را در پنجره شناور ادامه دهید</string>
<string name="pref_background_stop">تمام پخش را متوقف کنید</string>
<string name="pref_background_audio">به عنوان جریان صوتی پس زمینه ادامه دهید</string>
<string name="pref_description_language_app">انتخاب زبان برای رابط برنامه برای اعمال تغییرات ، برنامه را مجدداً راه اندازی کنید.</string>
<string name="settings_api_error_float">نگارش اندروید از ویدیوی شناور پشتیبانی نمی‌کند</string>
<string name="pref_background_behavior">پیکربندی پخش پس‌زمینه</string>
<string name="pref_background_float">ادامهٔ پخش ویدیو در پنجرهٔ شناور</string>
<string name="pref_background_stop">توقّف تمامی پخش</string>
<string name="pref_background_audio">ادامه به شکل جریان صوتی پس‌زمینه</string>
<string name="pref_description_language_app">گزینش زبان رابط برنامه.برای تأثیر گذاشتن تغییرات، کاره را دوباره آغاز کنید.</string>
<string name="pref_language_app">زبان برنامه</string>
<string name="pref_description_back_pause">هنگام فشار دادن به عقب در حین پخش ویدئو ، پخش پس زمینه را متوقف کنید.</string>
<string name="pref_title_back_pause">دکمه پشت مکث</string>
<string name="title_activity_url_video_play">فعالیت پخش ویدئو Url</string>
<string name="permission_rationale">برای تکمیل ایمیل مجوز تماس بگیرید.</string>
<string name="pref_description_back_pause">مکث پخش پس‌زمینه هنگام فشردن بازگشت حین پخش ویدیو.</string>
<string name="pref_title_back_pause">مکث با دکمهٔ بازگشت</string>
<string name="title_activity_url_video_play">UrlVideoPlayActivity</string>
<string name="permission_rationale">اعطای اجازهٔ آشنا برای تکمیل رایانامه.</string>
<string name="pref_title_video_speed">سرعت پخش پیش‌گزیده</string>
<string name="pref_description_video_speed">گزینش سرعت پخش ویدیوی عمومی</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="server_book_add_pick_server_button">Hae</string>
<string name="server_book_add_label">Leima</string>
<string name="video_list_live_marker">SUORA</string>
<string name="pref_title_video_speed">Oletusarvoinen toistonopeus</string>
<string name="pref_description_video_speed">Valitse yleinen videon toistonopeus</string>
<string name="action_bar_title_address_book">Osoitekirja</string>
</resources>

View File

@ -171,7 +171,7 @@
<string name="de">allemand</string>
<string name="gsg">langue des signes allemande</string>
<string name="gn">guarani</string>
<string name="gu">goudjarâtî</string>
<string name="gu">goudjarati</string>
<string name="ht">haïtien</string>
<string name="ha">haoussa</string>
<string name="he">hébreu</string>
@ -361,4 +361,7 @@
<string name="pref_title_accept_insecure">Désactiver la vérification des certificats SSL</string>
<string name="pref_description_accept_insecure">Ignorer les connexions non sécuritaires. Utiliser ceci seulement si vous connaissez le serveur sur lequel vous vous connectez. Requiert un redémarrage de l\'application.</string>
<string name="video_list_live_marker">DIRECT</string>
<string name="pref_title_video_speed">Vitesse de lecture par défaut</string>
<string name="pref_description_video_speed">Sélectionnez la vitesse globale de lecture vidéo</string>
<string name="action_bar_title_address_book">Carnet d\'adresses</string>
</resources>

View File

@ -131,7 +131,7 @@
<string name="video_speed_075">0.75x</string>
<string name="title_activity_server_address_book">Buku Alamat</string>
<string name="login_current_server_hint">Server Saat Ini</string>
<string name="server_book_list_has_login">Memiliki Login</string>
<string name="server_book_list_has_login">Telah Login</string>
<string name="server_book_add_save_button">Simpan</string>
<string name="server_book_add_add_button">Tambah</string>
<string name="server_book_add_password">Kata sandi</string>
@ -265,7 +265,7 @@
<string name="os">Ossetia</string>
<string name="om">Oromo</string>
<string name="or">Oriya (bahasa makro)</string>
<string name="teal">Teal</string>
<string name="teal">Sian Hijau</string>
<string name="sfs">Bahasa Isyarat Afrika Selatan</string>
<string name="so">Somalia</string>
<string name="sl">Slovenia</string>
@ -281,5 +281,66 @@
<string name="sc">Sardinia</string>
<string name="sg">Sango</string>
<string name="sm">Samoa</string>
<string name="bluegray">Bluegray</string>
<string name="bluegray">Abu-abu Biru</string>
<string name="ka">Georgia</string>
<string name="de">Jerman</string>
<string name="ik">Iñupiat (Alaska)</string>
<string name="ja">Jepang</string>
<string name="jv">Jawa (Indonesia)</string>
<string name="km">Kamboja</string>
<string name="lb">Luksembur</string>
<string name="mk">Macedonia</string>
<string name="mg">Madagaskar</string>
<string name="co">Korsika</string>
<string name="dz">Bhutan</string>
<string name="ff">Senegambia</string>
<string name="lg">Ganda</string>
<string name="gu">Gujarat (India)</string>
<string name="ht">Haiti</string>
<string name="ha">Hausa (Afro-Asia)</string>
<string name="he">Ibrani</string>
<string name="hz">Herero - Afrika Selatan</string>
<string name="hi">Hindi (India Utara)</string>
<string name="iu">Inuit (Kanada)</string>
<string name="ga">Irlandia</string>
<string name="it">Italia</string>
<string name="ms">Melayu</string>
<string name="mn">Mongolia</string>
<string name="el">Yunani Modern</string>
<string name="pref_description_license">
\n<b>Lisensi Publik Umum GNU Affero v3.0</b>
\n
\nIzin dari lisensi copyleft ini dikondisikan untuk menyediakan kode sumber yang lengkap dari karya berlisensi dan modifikasi, yang mencakup karya yang lebih besar menggunakan karya berlisensi, di bawah lisensi yang sama. Hak cipta dan pemberitahuan lisensi harus dipertahankan. Kontributor memberikan hibah hak paten secara tegas. Ketika versi modifikasi digunakan untuk menyediakan layanan melalui jaringan, kode sumber lengkap dari versi modifikasi harus tersedia.</string>
<string name="pref_background_audio">Lanjutkan stream suara di latar belakang</string>
<string name="gn">Guarani (Paraguai)</string>
<string name="pref_insecure_confirm_message">Anda akan menonaktifkan semua validasi Sertifikasi SSL di Thorium. Menonaktifkannya bisa sangat berbahaya jika server peertube tidak berada di bawah kendali Anda, karena serangan man-in-the-middle dapat mengarahkan lalu lintas ke server lain tanpa sepengetahuan Anda. Seorang penyerang dapat merekam kata sandi dan data pribadi lainnya.</string>
<string name="ki">Kikuyu (Kenya)</string>
<string name="rw">Rwanda</string>
<string name="tlh">StarTrek</string>
<string name="lv">Latvia</string>
<string name="ko">Korea</string>
<string name="lt">Lithuania</string>
<string name="kv">Komi (Rusia)</string>
<string name="kg">Kongo</string>
<string name="avk">Kotava</string>
<string name="am">Ethiopia</string>
<string name="av">Kaukasian</string>
<string name="ho">Papua Nugini</string>
<string name="is">Islandia</string>
<string name="ig">Igbo (Nigeria)</string>
<string name="id">Indonesia</string>
<string name="jsl">Bahasa Isyarat Jepang</string>
<string name="kl">Greenland</string>
<string name="kn">Kannada (barat daya India)</string>
<string name="kr">Kanuri (Nigeria)</string>
<string name="ks">Kasmir (India)</string>
<string name="kk">Kazakhstan</string>
<string name="pref_title_video_speed">Kecepatan putar standar</string>
<string name="pref_description_video_speed">Pilih secara umum kecepatan putar standar video</string>
<string name="pref_description_accept_insecure">Acuhkan koneksi yang tidak aman. Gunakan ini jika Anda tahu peladan yang anda tuju. Muat Ulang Aplikasi.</string>
<string name="dv">Maldiva</string>
<string name="gl">Galisia (barat laut Spanyol)</string>
<string name="hu">Hungaria</string>
<string name="ky">Kirgistan</string>
<string name="server_selection_nsfw_instance">Saluran NSFW</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="settings_activity_advanced_category_title">Avanzato</string>
<string name="server_book_add_save_button">Salva</string>
<string name="pref_description_accept_insecure">Ignora le connessioni insicure. Usalo solo se conosci il server a cui ti stai connettendo. Richiede il riavvio dell\'applicazione.</string>
<string name="action_bar_title_address_book">Rubrica</string>
<string name="pref_title_video_speed">Velocità di riproduzione predefinita</string>
<string name="pref_description_video_speed">Seleziona la velocità globale di riproduzione video</string>
</resources>

View File

@ -60,10 +60,10 @@
<string name="account_about_subscribers">Abonnenter:</string>
<string name="account_about_description">Beskrivelse:</string>
<string name="account_about_joined">Tok del:</string>
<string name="api_error">Noe gikk galt, prøv igjen senere.</string>
<string name="api_error">Noe gikk galt, prøv igjen senere!</string>
<string name="permission_rationale">Innvilg kontakttilgang for fullføring av e-postadresser.</string>
<string name="bottom_nav_title_trending">Populært</string>
<string name="meta_data_views">" visninger"</string>
<string name="meta_data_views">" Visninger"</string>
<string name="video_row_video_thumbnail">Video-miniatyrbilde</string>
<string name="pref_title_show_nsfw">VOKSENT innhold</string>
<string name="pref_description_show_nsfw">Vis VOKSENT innhold</string>
@ -103,7 +103,7 @@
<string name="server_selection_signup_allowed_yes">Ja</string>
<string name="server_selection_signup_allowed_no">Nei</string>
<string name="server_selection_set_server">Tjener satt til: %s</string>
<string name="server_selection_select_a_server">Velg en tjener fra listen, eller skriv den inn direkte</string>
<string name="server_selection_select_a_server">Velg en tjener fra listen under, eller skriv inn tjenernavn direkte.</string>
<string name="server_selection_peertube_server_url">PeerTube tjenernettadresse</string>
<string name="action_bar_title_server_selection">Velg tjener</string>
<string name="da">Dansk</string>
@ -139,7 +139,7 @@
<string name="pref_language_app">Programspråk</string>
<string name="server_book_add_server_url">Tjener-nettadresse</string>
<string name="server_book_add_pick_server_button">Søk</string>
<string name="server_book_add_username">Username</string>
<string name="server_book_add_username">Brukernavn</string>
<string name="server_book_add_add_button">Legg til</string>
<string name="server_book_add_password">Passord</string>
<string name="title_activity_server_address_book">Adressebok</string>
@ -164,7 +164,7 @@
<string name="me_logout_button">Logg ut</string>
<string name="server_book_valid_url_is_required">Gyldig nettadresse kreves</string>
<string name="server_book_label_is_required">Tjeneretikett kreves</string>
<string name="authentication_login_failed">Kunne ikke logge inn</string>
<string name="authentication_login_failed">Kunne ikke logge inn!</string>
<string name="authentication_login_success">Innlogget</string>
<string name="hello_blank_fragment">Hei blanke fragment</string>
<string name="network_error">Tilknytningsfeil, sjekk tilkoblingen din</string>
@ -191,8 +191,14 @@
<string name="pref_insecure_confirm_title">Advarsel!</string>
<string name="settings_activity_advanced_category_title">Avansert</string>
<string name="server_book_add_save_button">Lagre</string>
<string name="pref_insecure_confirm_message">Du er i ferd med å skru av all SSL-sertifisering i Thorium. Å skru av dette kan være veldig farlig hvis Peertube ikke er under din kontroll, fordi mellommanns-angrep kan sende trafikk til en annen tjener uten at du vet det. En angriper kan da se passordene når de blir brukt, og annen personlig data.</string>
<string name="pref_insecure_confirm_message">Du er i ferd med å skru av all kontroll av SSL-sertifikater i Thorium. Å skru av dette kan være veldig farlig hvis Peertube-tjeneren ikke er under din kontroll, fordi mellommanns-angrep kan styre trafikk til en annen tjener uten at du vet det. En angriper kan da registrere passord, og annen personlig data.</string>
<string name="video_list_live_marker">Sanntid</string>
<string name="pref_title_accept_insecure">Skru av SSL-sertifikatssjekk</string>
<string name="pref_description_accept_insecure">Ignorer usikre tilkoblinger. Kun bruk dette hvis du vet hvilken tjener du kobler til. Krever programomstart.</string>
<string name="pref_description_accept_insecure">Ignorer usikre tilkoblinger. Bruk kun dette hvis du vet hvilken tjener du kobler til. Krever programomstart.</string>
<string name="es">Spansk</string>
<string name="id">Indonesisk</string>
<string name="action_bar_title_address_book">Adressebok</string>
<string name="pref_title_video_speed">Forvalgt avspillingshastighet</string>
<string name="pref_description_video_speed">Velg videoavspillingshastighet for hele systemet</string>
<string name="ab">Abkhasisk</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="pref_title_accept_insecure">Desativar check do certificado SSL</string>
<string name="pref_description_accept_insecure">Ignorar conexões não seguras. Use isto apenas se você conhece o servidor ao qual está se conectando. Requer o reinício do aplicativo.</string>
<string name="video_list_live_marker">AO VIVO</string>
<string name="pref_title_video_speed">Velocidade de reprodução padrão</string>
<string name="pref_description_video_speed">Selecione a velocidade global de reprodução de vídeo</string>
<string name="action_bar_title_address_book">Lista de endereços</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="server_book_add_save_button">Guardar</string>
<string name="pref_title_accept_insecure">Desativar a verificação do certificado SSL</string>
<string name="pref_description_accept_insecure">Ignorar conexões inseguras. Use isto apenas se souber a qual servidor está a conectar-se. Requer reinicialização da aplicação.</string>
<string name="pref_title_video_speed">Velocidade de reprodução predefinida</string>
<string name="pref_description_video_speed">Selecione a velocidade de reprodução de vídeo global</string>
<string name="action_bar_title_address_book">Lista de contactos</string>
</resources>

View File

@ -362,4 +362,7 @@
<string name="pref_insecure_confirm_message">"Вы собираетесь отключить валидацию всех SSL сертификатов в Thorium. Это может быть очень опасно если peertube сервер вами не контролируется, потому что \"атака посредника\" может направить трафик на другой сервер. Злоумышленник может записывать пароли и другие личные данные."</string>
<string name="server_book_add_save_button">Сохранить</string>
<string name="video_list_live_marker">В ЭФИРЕ</string>
<string name="action_bar_title_address_book">Адресная книга</string>
<string name="pref_title_video_speed">Скорость воспроизведения по умолчанию</string>
<string name="pref_description_video_speed">Выберите глобальную скорость воспроизведения видео</string>
</resources>

View File

@ -1,2 +1,50 @@
<?xml version="1.0" encoding="utf-8"?>
<resources></resources>
<resources>
<string name="title_activity_login">Intra</string>
<string name="prompt_password">Crae</string>
<string name="action_bar_title_search">Chirca</string>
<string name="action_bar_title_account">Contu</string>
<string name="bottom_nav_title_account">Contu</string>
<string name="action_bar_title_settings">impostatziones</string>
<string name="action_sign_in">Intra</string>
<string name="action_sign_in_short">Intra</string>
<string name="bottom_nav_title_recent">Reghente</string>
<string name="bottom_nav_title_local">Locale</string>
<string name="descr_overflow_button">Àteru</string>
<string name="menu_share">Cumpartzi</string>
<string name="pref_title_dark_mode">Modalidade iscura</string>
<string name="pref_title_app_theme">Tema de s\'aplicatzione</string>
<string name="pref_title_torrent_player">Riprodusidore de vìdeu Torrent</string>
<string name="pref_title_version">Versione</string>
<string name="pref_title_show_nsfw">Cuntenutu pro adultos</string>
<string name="pref_description_show_nsfw">Ammustra cuntenutu pro adultos</string>
<string name="pref_title_peertube_server">Serbidore PeerTube</string>
<string name="invalid_url">URL non bàlidu.</string>
<string name="pref_title_background_play">Riprodutzione in isfundu</string>
<string name="account_about_account">Contu:</string>
<string name="account_about_subscribers">Sutiscritos:</string>
<string name="server_book_add_username">Nòmine usuàriu</string>
<string name="server_book_add_password">Crae</string>
<string name="server_book_add_add_button">Annanghe</string>
<string name="settings_activity_about_category_title">A pitzu de</string>
<string name="pref_insecure_confirm_title">Atentzione!</string>
<string name="title_activity_search">Chirca</string>
<string name="title_activity_settings">Impostatziones</string>
<string name="search_hint">Chirca PeerTube</string>
<string name="prompt_server">Serbidore</string>
<string name="no_data_available">Perunu resurtadu</string>
<string name="pref_language">Filtru de limba</string>
<string name="pref_title_license">Litzèntzia</string>
<string name="pref_insecure_confirm_yes">Eja</string>
<string name="server_book_add_label">Eticheta</string>
<string name="account_about_description">Descritzione:</string>
<string name="account_bottom_menu_about">A pitzu de</string>
<string name="server_book_add_pick_server_button">Chirca</string>
<string name="server_book_add_save_button">Sarva</string>
<string name="video_speed_075">0.75x</string>
<string name="video_speed_125">1.25x</string>
<string name="title_activity_settings2">ImpostatzionesAtividade2</string>
<string name="title_activity_me">Contu</string>
<string name="pref_insecure_confirm_no">Nono</string>
<string name="video_list_live_marker">IN DIRETA</string>
</resources>

View File

@ -89,7 +89,7 @@
<string name="da">Danimarkaca</string>
<string name="dsl">Danimarka İşaret Dili</string>
<string name="dv">Maldivce</string>
<string name="nl">Flemenkçe</string>
<string name="nl">Felemenkçe</string>
<string name="dz">Dzongka</string>
<string name="en">İngilizce</string>
<string name="eo">Esperanto</string>
@ -371,4 +371,7 @@
<string name="pref_description_accept_insecure">Güvenli olmayan bağlantıları yok sayın. Bunu yalnızca bağlandığınız sunucuyu biliyorsanız kullanın. Uygulamanın yeniden başlatılmasını gerektirir.</string>
<string name="server_book_add_save_button">Kaydet</string>
<string name="video_list_live_marker">CANLI</string>
<string name="pref_title_video_speed">Öntanımlı Oynatma Hızı</string>
<string name="pref_description_video_speed">Genel Video Oynatma Hızını Seçin</string>
<string name="action_bar_title_address_book">Adres Defteri</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="settings_activity_advanced_category_title">Додатково</string>
<string name="server_book_add_save_button">Зберегти</string>
<string name="video_list_live_marker">НАЖИВО</string>
<string name="pref_title_video_speed">Типова швидкість відтворення</string>
<string name="pref_description_video_speed">Вибрати загальну швидкість відтворення відео</string>
<string name="action_bar_title_address_book">Адресна книга</string>
</resources>

View File

@ -127,7 +127,7 @@
<string name="server_selection_select_a_server">从下面的列表选择一个服务器或者手动输入。</string>
<string name="server_selection_peertube_server_url">PeerTube 服务器 URL</string>
<string name="action_bar_title_server_selection">选择服务器</string>
<string name="login_current_server_hint">现在的服务器</string>
<string name="login_current_server_hint">当前的服务器</string>
<string name="video_speed_075">0.75倍速</string>
<string name="video_speed_125">1.25倍速</string>
<string name="pt">葡萄牙语</string>
@ -308,4 +308,33 @@
<string name="ro">罗马尼亚语</string>
<string name="qu">克丘亚语</string>
<string name="ps">普什图语</string>
<string name="me_help_and_feedback_button">帮助与反馈</string>
<string name="authentication_login_failed">登录失败!</string>
<string name="server_selection_filter_hint">过滤列表</string>
<string name="pref_title_video_speed">默认播放速度</string>
<string name="settings_activity_about_category_title">关于</string>
<string name="server_book_del_alert_msg">您确定您想从地址薄中移除该服务器吗?</string>
<string name="pref_description_video_speed">选择全局视频播放速度</string>
<string name="server_book_add_label">标签</string>
<string name="server_book_add_server_url">服务器 URL</string>
<string name="server_book_add_pick_server_button">搜索</string>
<string name="server_book_add_username">用户名</string>
<string name="server_book_add_password">密码</string>
<string name="server_book_add_add_button">添加</string>
<string name="server_book_add_save_button">保存</string>
<string name="server_book_list_has_login">已登录</string>
<string name="title_activity_server_address_book">地址薄</string>
<string name="server_book_label_is_required">服务器标签必填</string>
<string name="network_error">网络访问错误,请检查您的网络连接</string>
<string name="authentication_login_success">已登录</string>
<string name="me_logout_button">退出登录</string>
<string name="server_book_del_alert_title">移除服务器</string>
<string name="title_activity_select_server">搜索服务器</string>
<string name="title_activity_me">账号</string>
<string name="settings_activity_video_playback_category_title">视频播放</string>
<string name="server_book_valid_url_is_required">有效的 URL 必填</string>
<string name="settings_activity_video_list_category_title">视频列表</string>
<string name="pref_insecure_confirm_yes"></string>
<string name="pref_insecure_confirm_no"></string>
<string name="video_list_live_marker">直播</string>
</resources>

View File

@ -356,4 +356,7 @@
<string name="pref_description_accept_insecure">忽略不安全的連線。僅在您了解您要連線的伺服器時才使用此選項。需要重新啟動應用程式。</string>
<string name="server_book_add_save_button">儲存</string>
<string name="video_list_live_marker">直播</string>
<string name="pref_title_video_speed">預設播放速度</string>
<string name="pref_description_video_speed">選取全域影片播放速度</string>
<string name="action_bar_title_address_book">通訊錄</string>
</resources>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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'

View File

@ -1,4 +1,4 @@
# 1.1.0 (01-02-2021)
# 1.1.0 (2021-02-01)
### Características

View File

@ -1,33 +1,33 @@
Thorium یک سرویس گیرنده PeerTube است که می تواند به هر سرور peertube با نسخه v1.1.0-alpha.2 یا بالاتر متصل شود.
توریوم یک کارخواه پیرتیوب است که می تواند به هر کارساز پیرتیوبی با نگارش v1.1.0-alpha.2 یا بالاتر وصل شود.
PeerTube یک پلتفرم متحد (ActivityPub) پخش ویدئو با استفاده از P2P (BitTorrent) مستقیماً در مرورگر وب است. برای اطلاعات بیشتر ، لطفاً برای اطلاعات بیشتر و لیستی از سرورها به https://joinpeertube.org/ مراجعه کنید.
پیرتیوب یک بن‌سازهٔ جریان ویدیوی خودگردان (ActivityPub) با استفاده از P2P (بیت‌تورنت) مستقیم در مرورگر وب است. برای اطلاعات بیشتر ، لطفاً به https://joinpeertube.org مراجعه کنید.
این سرویس گیرنده با یک سرور PeerTube که توسط خالق برنامه مدیریت می شود - و نه خود پروژه PeerTube ، که در http://inances.joinpeertube.org/ فهرست شده است - از پیش تنظیم شده است - به شما این امکان را می دهد تا طعم و مزه کارهایی که مشتری قادر به انجام آن است را بشنوید. سرور خود را برای تنظیم تجربه خود انتخاب کنید!
این کارخواه با کارساز پیرتیوبی که به دست خالق برنامه مدیریت می‌شود (و نه خود پروژه پیرتیوب ، که در http://inances.joinpeertube.org فهرست شده) از پیش تنظیم شده تا بگذارد طعم قابلیت‌های کارخواه را بچشید. برای تنظیم تجربه‌تان، کارساز را برگزینید!
ویژگی های فعلی:
- اتصال به هر سرور PeerTube
- ویدئو تورنت یا پخش مستقیم
- جستجو در PeerTube
- اتصال به هر کارساز پیرتیوب
- ویدیوی تورنت یا پخش مستقیم
- جست‌وجو‌در پیرتیوب
- بارگیری / اشتراک ویدیو
- تم / حالت تاریک
- زمینه / حالت تاریک
- پخش در پس زمینه
- پخش تمام صفحه در حالت افقی
- سرعت پخش
- محتوای NSFW را فیلتر کنید
- پالایش محتوای NSFW
- احراز هویت / ورود
- دوست داشتن/دوست نداشتن ویدئو
- پسندیدن / نپسندیدن ویدیو
به زودی:
- فیلم های نظر دهید
- نظر دادن به ویدیو
- ثبت نام
- صفحه نمای کلی کاربر / کانال
- گزارش فیلم ها
- گزارش ویدیوها
مجوزها:
- دسترسی به ذخیره سازی ، مورد نیاز برای بارگیری تورنت یا بارگیری ویدئو.
اجازه‌ها:
- دسترسی به ذخیره سازی ، مورد نیاز برای بارگیری تورنت یا بارگیری ویدیو.
دارای مجوز تحت GNU Affero General Public License v3.0
دارای پروانهٔ GNU Affero General Public License v3.0
مجوزهای این قوی ترین مجوز copyleft منوط به در دسترس قرار دادن کد منبع کامل آثار مجاز و اصلاحات ، که شامل آثار بزرگتر با استفاده از یک اثر مجاز ، تحت همان مجوز است. اخطار حق نسخه برداری و مجوز باید حفظ شود. مشارکت کنندگان اعطای صریح حقوق ثبت اختراع را ارائه می دهند. هنگامی که از نسخه اصلاح شده برای ارائه خدمات در شبکه استفاده می شود ، باید کد منبع کامل نسخه اصلاح شده در دسترس باشد.
مجوزهای این قوی ترین پروانهٔ copyleft منوط به در دسترس قرار دادن کد منبع کامل آثار مجاز و اصلاحات ، که شامل آثار بزرگتر با استفاده از یک اثر مجاز ، تحت همان پروانه است. اخطار حق نسخه برداری و مجوز باید حفظ شود. مشارکت کنندگان اعطای صریح حقوق ثبت اختراع را ارائه می دهند. هنگامی که از نسخه اصلاح شده برای ارائه خدمات در شبکه استفاده می شود ، باید کد منبع کامل نسخه اصلاح شده در دسترس باشد.
کد منبع در: https://github.com/sschueller/peertube-android/
کد مبدأ در: https://github.com/sschueller/peertube-android/

View File

@ -1 +1 @@
Thorium یک پخش کننده PeerTube غیر رسمی است
توریوم پخش‌کننده‌ای غیررسمی برای پیرتیوب است

View File

@ -0,0 +1,6 @@
# 1.2.0 (2021-02-07)
### Fonctionnalités
* Marquer les vidéos en direct dans les listes de vidéos 8518b80

View File

@ -0,0 +1,12 @@
# 1.3.0 (2021-02-13)
### Corrections de Bug
* Ai converti videolist en Kotlin pour réparer le menu du haut cassé 06ace0d
### Fonctionnalités
* Ajout du placeholder pour la miniature de la vidéo lors du chargement et en cas d'erreurs 830b197
* Convertion du helper de "meta date" en Kotlin 1c34556

View File

@ -1,6 +1,6 @@
Thorium est un client PeerTube qui peut se connecter à tout serveur PeerTube exécutant la version v1.1.0-alpha.2 ou supérieure.
PeerTube est une plateforme de streaming vidéo fédérée (ActivityPub) utilisant le P2P (BitTorrent) directement dans le navigateur web. Pour plus d'informations, veuillez visiter https://joinpeertube.org/ pour plus d'informations et une liste de serveurs.
PeerTube est une plateforme de streaming vidéo fédérée (ActivityPub) utilisant le P2P (BitTorrent) directement dans le navigateur web. Pour plus d'informations, veuillez visiter https://joinpeertube.org/
Ce client est livré préconfiguré avec un serveur PeerTube géré par le créateur de l'application - et non par le projet PeerTube lui-même, dont la liste est disponible sur http://instances.joinpeertube.org/ - afin de vous permettre d'avoir un avant-goût de ce dont le client est capable. Choisissez votre serveur pour affiner votre expérience !

View File

@ -0,0 +1 @@
- f-droid dikeluarkan untuk memperbaharui deploy otomatis

View File

@ -1,6 +1,6 @@
Thorium adalah klien PeerTube yang dapat terhubung ke server peertube yang menjalankan versi v1.1.0-alpha.2 atau yang lebih tinggi.
Thorium adalah klien PeerTube yang dapat terhubung ke jaringan PeerTube yang menjalankan versi v1.1.0-alpha.2 atau yang lebih tinggi.
PeerTube adalah platform streaming video federated (ActivityPub) menggunakan P2P (BitTorrent) via browser web. Untuk informasi lebih lanjut, silakan kunjungi https://joinpeertube.org/ .
PeerTube adalah platform federasi streaming video (ActivityPub) menggunakan P2P (BitTorrent) via browser web. Untuk informasi lebih lanjut, silakan kunjungi https://joinpeertube.org/ .
Klien ini hadir dengan prakonfigurasi satu server PeerTube yang dikelola oleh pembuat aplikasi - bukan proyek PeerTube itu sendiri, lihat daftar lainnya di http://instances.joinpeertube.org/ - untuk memungkinkan Anda memiliki selera apa yang mampu dilakukan klien. Pilih server Anda untuk menyelaraskan pengalaman Anda!

View File

@ -1 +1 @@
Thorium adalah unoffical pemutar PeerTube
Thorium adalah Aplikasi pemutar media bukan dari PeerTube

View File

@ -1,6 +1,6 @@
Thorium è un client PeerTube in grado di connettersi a qualsiasi server peertube che esegue la versione v1.1.0-alpha.2 o successiva.
PeerTube è una piattaforma di streaming video federata (ActivityPub) che utilizza P2P (BitTorrent) direttamente nel browser web. Per ulteriori informazioni, visitare https://joinpeertube.org/ per ulteriori informazioni e un elenco dei server.
PeerTube è una piattaforma di streaming video federata (ActivityPub) che utilizza P2P (BitTorrent) direttamente nel browser web. Per ulteriori informazioni, visitare https://joinpeertube.org/
Questo client viene preconfigurato con un server PeerTube gestito dal creatore dell'applicazione non il progetto PeerTube stesso, che elenca di più su http://instances.joinpeertube.org/ per consentire di avere un assaggio di ciò che il client è in grado di. Scegli il tuo server per ottimizzare la tua esperienza!

View File

@ -0,0 +1 @@
- f-droid-utgave for å fikse automatisk utrulling

View File

@ -0,0 +1 @@
- La til støtte for HLS-avspilling

View File

@ -0,0 +1 @@
- Atualização de autenticação

View File

@ -0,0 +1,33 @@
Thorium é um cliente PeerTube que pode se conectar a qualquer servidor PeerTube rodando a versão v1.1.0-alpha.2 ou superior.
PeerTube é uma plataforma de transmissão de vídeo federada (ActivityPub) usando P2P (BitTorrent) diretamente no seu navegador. Para mais informações, visite https://joinpeertube.org/.
Este app vem pré-configurado com um servidor PeerTube gerido pelo criador da aplicação - não pelo próprio projeto PeerTube, que lista mais servidores em http://instances.joinpeertube.org/ - para lhe permitir ter uma ideia do que o app é capaz de fazer. Escolha o seu servidor para adaptar à sua experiência!
Características atuais:
- Conecte-se a qualquer servidor PeerTube
- Baixe ou reproduza vídeos diretamente via Torrent
- Pesquise no PeerTube
- Baixe / Compartilhe vídeos
- Temas / Modo escuro
- Reprodução de fundo
- Reprodução em tela cheia no modo paisagem
- Controle da velocidade de reprodução
- Filtro de conteúdo NSFW
- Autenticação / Login
- Likes/dislikes nos vídeos
Em breve:
- Comente vídeos
- Registre-se
- Página de Visão Geral do Usuário / Canal
- Reportar Vídeos
Permissões:
- Acesso ao armazenamento, necessário para download de torrent ou download de vídeo.
Licenciado sob a GNU Affero General Public License v3.0
As permissões desta licença mais forte de copyleft estão condicionadas a disponibilizar o código fonte completo de obras licenciadas e modificações, que incluem obras maiores usando uma obra licenciada, sob a mesma licença. Os avisos de direitos autorais e de licença devem ser preservados. Os contribuidores fornecem uma concessão expressa de direitos de patente. Quando uma versão modificada é usada para fornecer um serviço através de uma rede, o código fonte completo da versão modificada deve ser disponibilizado.
Código fonte em: https://github.com/sschueller/peertube-android/

View File

@ -0,0 +1 @@
https://www.youtube.com/watch?v=PJIsiuSdpq8

View File

@ -0,0 +1,6 @@
# 1.2.0 (2021-02-07)
### Funcionalidades
* Marcar vídeos ao vivo em listas de vídeos 8518b80

View File

@ -0,0 +1,12 @@
# 1.3.0 (2021-02-13)
### Correções
* Lista de vídeos convertidas em Kotlin para corrigir o menu no topo 06ace0d
### Recursos
* Adicionado local para miniaturas de vídeo para carregamento e erros 830b197
* Ajudante de metadados convertido em kotlin 1c34556

View File

@ -0,0 +1,11 @@
# 1.4.0 (2021-02-20)
### Correções
* Falha de dispositivos SDK 21,22,23,24 no início, correções [# 262] (https://git.techdroid.com/sschueller/peertube/issues/262) 5622b76
### Recursos
* adicionada configuração de velocidade de reprodução global fa79b2d

View File

@ -0,0 +1 @@
- Atualização da autenticação

View File

@ -0,0 +1 @@
- lançamento f-droid para corrigir a implantação automática

View File

@ -0,0 +1,7 @@
- adicionar suporte de redirecionamento de hipertexto na descrição (@freeboub)
- várias correções de bloqueio (@freeboub)
- evitar ir para o 'pip' ao sair da aplicação devido ao botão de partilha (@freeboub)
- adicionada capacidade de filtrar a lista de servidores (@freeboub)
- refatorização da gestão de erros Toast para dividir o erro da rede (@freeboub)
- manter a proporção do vídeo para 'pip' (@freeboub)
- barra de navegação não foi restaurada ao deixar o modo paisagem (@freeboub)

View File

@ -0,0 +1,2 @@
- adicionado suporte para desativar o SSL
- traduções

View File

@ -0,0 +1,5 @@
- idioma padrão da aplicação fixo na primeira inicialização (@kosharskiy)
- traduções do ecrã de definições em uk e ru (@kosharskiy)
- ficheiro de limpeza app/build.gradle (@kosharskiy)
- problema de visualização de dados meta de vídeo fixo (@kosharskiy)
- traduções atualizadas

View File

@ -0,0 +1,2 @@
- servidor de edição implementado no livro do servidor (@kosharskiy)
- traduções atualizadas

View File

@ -0,0 +1,7 @@
- Fazer X no modo 'pip' para o áudio de fundo corretamente (@dhk2)
- Adicionada a opção clara de histórico de pesquisa ao menu de configurações (@dhk2)
- Não corrigir nenhum idioma selecionado por padrão para todos os idiomas de vídeo
- Biblioteca de ícones atualizada
- Adicionado indicador de 'buffer' à reprodução de vídeo
- Corrigidos problemas de vídeo em branco nos servidores que fornecem vídeo 0p.
- Traduções atualizadas

View File

@ -0,0 +1 @@
- Adicionado suporte a reprodução HLS

View File

@ -0,0 +1 @@
- Corrigido modelo incorreto impedindo a reprodução de vídeo

View File

@ -0,0 +1,6 @@
# 1.1.0 (2021-02-01)
### Características
* **lang:** Finlandês adicionado 02bcd74

View File

@ -0,0 +1,6 @@
## 1.1.1 (2021-02-05)
### Correção de erros
* Removido SHA do nome da versão para corrigir as compilações fdroid 9dc7d54

View File

@ -0,0 +1,6 @@
# 1.2.0 (2021-02-07)
### Características
* Marcar vídeos ao vivo em listas de vídeo 8518b80

View File

@ -0,0 +1,12 @@
# 1.3.0 (2021-02-13)
### Correções
* Lista de vídeos convertidas em Kotlin para corrigir o menu no topo 06ace0d
### Recursos
* Adicionado local para miniaturas de vídeo para carregamento e erros 830b197
* Ajudante de metadados convertido em kotlin 1c34556

Some files were not shown because too many files have changed in this diff Show More