Merge branch 'develop' into 'master'
Release See merge request sschueller/peertube!47
This commit is contained in:
commit
b29afe52a2
@ -8,7 +8,7 @@ ENV ANDROID_SDK_CHECKSUM 124f2d5115eee365df6cf3228ffbca6fc3911d16f8025bebd5b1c6e
|
||||
# higher version casues Warning: Failed to find package
|
||||
ENV ANDROID_BUILD_TOOLS_VERSION 30.0.2
|
||||
ENV ANDROID_SDK_ROOT /usr/local/android-sdk-linux
|
||||
ENV ANDROID_VERSION 30
|
||||
ENV ANDROID_VERSION 32
|
||||
# ENV PATH ${PATH}:${ANDROID_HOME}/tools:${ANDROID_HOME}/platform-tools
|
||||
ENV PATH ${PATH}:${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin:${ANDROID_SDK_ROOT}/cmdline-tools/tools/bin
|
||||
|
||||
|
@ -39,13 +39,13 @@ else {
|
||||
apply plugin: 'kotlin-kapt'
|
||||
|
||||
android {
|
||||
compileSdkVersion 30
|
||||
compileSdkVersion 32
|
||||
buildToolsVersion "30.0.2"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "net.schueller.peertube"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 30
|
||||
targetSdkVersion 32
|
||||
versionCode 1069
|
||||
versionName "1.8.3"
|
||||
buildConfigField "long", "BUILD_TIME", readPropertyWithDefault('buildTimestamp', System.currentTimeMillis()) + 'L'
|
||||
@ -94,10 +94,10 @@ android {
|
||||
|
||||
}
|
||||
|
||||
def room_version = "2.3.0"
|
||||
def lifecycleVersion = '2.3.1'
|
||||
def exoplayer = '2.12.3'
|
||||
def fragment_version = "1.3.6"
|
||||
def room_version = "2.4.0"
|
||||
def lifecycleVersion = '2.4.0'
|
||||
def exoplayer = '2.16.1'
|
||||
def fragment_version = "1.4.0"
|
||||
|
||||
dependencies {
|
||||
implementation fileTree(dir: 'libs', include: ['*.jar'])
|
||||
@ -105,8 +105,8 @@ dependencies {
|
||||
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
|
||||
|
||||
// Layouts and design
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.1'
|
||||
implementation 'androidx.appcompat:appcompat:1.3.1'
|
||||
implementation 'androidx.constraintlayout:constraintlayout:2.1.2'
|
||||
implementation 'androidx.appcompat:appcompat:1.4.0'
|
||||
implementation 'com.google.android.material:material:1.4.0'
|
||||
implementation "androidx.swiperefreshlayout:swiperefreshlayout:1.1.0"
|
||||
implementation "androidx.fragment:fragment-ktx:$fragment_version"
|
||||
@ -118,7 +118,7 @@ dependencies {
|
||||
implementation 'com.mikepenz:fontawesome-typeface:5.9.0.2-kotlin@aar'
|
||||
|
||||
// http client / REST
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.1'
|
||||
implementation 'com.squareup.okhttp3:okhttp:4.9.2'
|
||||
implementation 'com.squareup.retrofit2:retrofit:2.9.0'
|
||||
|
||||
// image downloading and caching library
|
||||
|
@ -41,7 +41,7 @@ import java.util.*
|
||||
|
||||
class ServerAddressBookActivity : CommonActivity() {
|
||||
|
||||
private val TAG = "ServerAddressBookActivity"
|
||||
private val TAG = "ServerAddBookAct"
|
||||
|
||||
private val mServerViewModel: ServerViewModel by viewModels()
|
||||
private var addServerFragment: AddServerFragment? = null
|
||||
@ -133,15 +133,15 @@ class ServerAddressBookActivity : CommonActivity() {
|
||||
AlertDialog.Builder(this@ServerAddressBookActivity)
|
||||
.setTitle(getString(R.string.server_book_del_alert_title))
|
||||
.setMessage(getString(R.string.server_book_del_alert_msg))
|
||||
.setPositiveButton(android.R.string.yes) { _: DialogInterface?, _: Int ->
|
||||
val position = viewHolder.adapterPosition
|
||||
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
|
||||
val position = viewHolder.bindingAdapterPosition
|
||||
val server = adapter.getServerAtPosition(position)
|
||||
// Toast.makeText(ServerAddressBookActivity.this, "Deleting " +
|
||||
// server.getServerName(), Toast.LENGTH_LONG).show();
|
||||
// Delete the server
|
||||
mServerViewModel.delete(server)
|
||||
}
|
||||
.setNegativeButton(android.R.string.no) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.adapterPosition) }
|
||||
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
|
||||
.setIcon(android.R.drawable.ic_dialog_alert)
|
||||
.show()
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ package net.schueller.peertube.activity
|
||||
import android.Manifest.permission
|
||||
import android.R.drawable
|
||||
import android.R.string
|
||||
import android.app.Activity
|
||||
import android.app.AlertDialog.Builder
|
||||
import android.app.SearchManager
|
||||
import android.content.Context
|
||||
@ -33,6 +34,7 @@ import android.view.MenuItem
|
||||
import android.view.MenuItem.OnActionExpandListener
|
||||
import android.view.View
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.appcompat.widget.SearchView.OnSuggestionListener
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
@ -116,7 +118,7 @@ class VideoListActivity : CommonActivity() {
|
||||
Builder(this@VideoListActivity)
|
||||
.setTitle(getString(R.string.clear_search_history))
|
||||
.setMessage(getString(R.string.clear_search_history_prompt))
|
||||
.setPositiveButton(string.yes) { _, _ ->
|
||||
.setPositiveButton(string.ok) { _, _ ->
|
||||
val suggestions = SearchRecentSuggestions(
|
||||
applicationContext,
|
||||
SearchSuggestionsProvider.AUTHORITY,
|
||||
@ -124,7 +126,7 @@ class VideoListActivity : CommonActivity() {
|
||||
)
|
||||
suggestions.clearHistory()
|
||||
}
|
||||
.setNegativeButton(string.no, null)
|
||||
.setNegativeButton(string.cancel, null)
|
||||
.setIcon(drawable.ic_dialog_alert)
|
||||
.show()
|
||||
true
|
||||
@ -160,8 +162,7 @@ class VideoListActivity : CommonActivity() {
|
||||
position
|
||||
) as Cursor
|
||||
return cursor.getString(
|
||||
cursor
|
||||
.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
|
||||
cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
|
||||
)
|
||||
}
|
||||
|
||||
@ -178,15 +179,26 @@ class VideoListActivity : CommonActivity() {
|
||||
stopService(Intent(this, VideoPlayerService::class.java))
|
||||
}
|
||||
|
||||
public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
if (requestCode == SWITCH_INSTANCE) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
// public override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
// super.onActivityResult(requestCode, resultCode, data)
|
||||
// if (requestCode == SWITCH_INSTANCE) {
|
||||
// if (resultCode == RESULT_OK) {
|
||||
// loadVideos(currentStart, count, sort, filter)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
private var resultLauncher = registerForActivityResult(
|
||||
ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
}
|
||||
|
||||
private fun openActivityForResult(intent: Intent) {
|
||||
resultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
// Handle action bar item clicks here. The action bar will
|
||||
// automatically handle clicks on the Home/Up button, so long
|
||||
@ -213,7 +225,7 @@ class VideoListActivity : CommonActivity() {
|
||||
}
|
||||
id.action_server_address_book -> {
|
||||
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
|
||||
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
|
||||
openActivityForResult(addressBookActivityIntent)
|
||||
return false
|
||||
}
|
||||
else -> {
|
||||
@ -461,7 +473,7 @@ class VideoListActivity : CommonActivity() {
|
||||
// new IconicsDrawable(this, FontAwesome.Icon.faw_user_circle));
|
||||
|
||||
// Click Listener
|
||||
navigation.setOnNavigationItemSelectedListener { menuItem: MenuItem ->
|
||||
navigation.setOnItemSelectedListener { menuItem: MenuItem ->
|
||||
when (menuItem.itemId) {
|
||||
id.navigation_overview -> {
|
||||
// TODO
|
||||
@ -470,7 +482,7 @@ class VideoListActivity : CommonActivity() {
|
||||
loadOverview(currentPage)
|
||||
overViewActive = true
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_trending -> {
|
||||
//Log.v(TAG, "navigation_trending");
|
||||
@ -482,7 +494,7 @@ class VideoListActivity : CommonActivity() {
|
||||
subscriptions = false
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_recent -> {
|
||||
if (!isLoading) {
|
||||
@ -493,7 +505,7 @@ class VideoListActivity : CommonActivity() {
|
||||
subscriptions = false
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_local -> {
|
||||
//Log.v(TAG, "navigation_trending");
|
||||
@ -505,15 +517,15 @@ class VideoListActivity : CommonActivity() {
|
||||
subscriptions = false
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
id.navigation_subscriptions -> //Log.v(TAG, "navigation_subscriptions");
|
||||
if (!Session.getInstance().isLoggedIn) {
|
||||
// Intent intent = new Intent(this, LoginActivity.class);
|
||||
// this.startActivity(intent);
|
||||
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
|
||||
this.startActivityForResult(addressBookActivityIntent, SWITCH_INSTANCE)
|
||||
return@setOnNavigationItemSelectedListener false
|
||||
openActivityForResult(addressBookActivityIntent)
|
||||
return@setOnItemSelectedListener false
|
||||
} else {
|
||||
if (!isLoading) {
|
||||
overViewActive = false
|
||||
@ -523,7 +535,7 @@ class VideoListActivity : CommonActivity() {
|
||||
subscriptions = true
|
||||
loadVideos(currentStart, count, sort, filter)
|
||||
}
|
||||
return@setOnNavigationItemSelectedListener true
|
||||
return@setOnItemSelectedListener true
|
||||
}
|
||||
}
|
||||
false
|
||||
@ -574,6 +586,5 @@ class VideoListActivity : CommonActivity() {
|
||||
|
||||
const val EXTRA_VIDEOID = "VIDEOID"
|
||||
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
|
||||
const val SWITCH_INSTANCE = 2
|
||||
}
|
||||
}
|
@ -1,502 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package net.schueller.peertube.activity;
|
||||
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.app.AppOpsManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.PictureInPictureParams;
|
||||
import android.app.RemoteAction;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.drawable.Icon;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
|
||||
import android.preference.PreferenceManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
|
||||
import android.text.TextUtils;
|
||||
import android.util.Log;
|
||||
import android.util.Rational;
|
||||
import android.util.TypedValue;
|
||||
|
||||
import android.view.WindowManager;
|
||||
import android.widget.FrameLayout;
|
||||
|
||||
import android.widget.RelativeLayout;
|
||||
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment;
|
||||
import net.schueller.peertube.fragment.VideoPlayerFragment;
|
||||
import net.schueller.peertube.service.VideoPlayerService;
|
||||
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
|
||||
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PAUSE;
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_PLAY;
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
|
||||
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
|
||||
|
||||
public class VideoPlayActivity extends AppCompatActivity {
|
||||
|
||||
private static final String TAG = "VideoPlayActivity";
|
||||
|
||||
static boolean floatMode = false;
|
||||
|
||||
private static final int REQUEST_CODE = 101;
|
||||
private BroadcastReceiver receiver;
|
||||
|
||||
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
|
||||
@SuppressLint("NewApi")
|
||||
public void makePipControls() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
ArrayList<RemoteAction> actions = new ArrayList<>();
|
||||
|
||||
Intent actionIntent = new Intent(getString(R.string.app_background_audio));
|
||||
PendingIntent pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
@SuppressLint({"NewApi", "LocalSuppress"}) Icon icon = Icon.createWithResource(getApplicationContext(), android.R.drawable.stat_sys_speakerphone);
|
||||
@SuppressLint({"NewApi", "LocalSuppress"}) RemoteAction remoteAction = new RemoteAction(icon, "close pip", "from pip window custom command", pendingIntent);
|
||||
actions.add(remoteAction);
|
||||
|
||||
actionIntent = new Intent(ACTION_STOP);
|
||||
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop);
|
||||
remoteAction = new RemoteAction(icon, "play", "stop the media", pendingIntent);
|
||||
actions.add(remoteAction);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
if (videoPlayerFragment.isPaused()) {
|
||||
Log.e(TAG, "setting actions with play button");
|
||||
actionIntent = new Intent(ACTION_PLAY);
|
||||
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_play);
|
||||
remoteAction = new RemoteAction(icon, "play", "play the media", pendingIntent);
|
||||
} else {
|
||||
Log.e(TAG, "setting actions with pause button");
|
||||
actionIntent = new Intent(ACTION_PAUSE);
|
||||
pendingIntent = PendingIntent.getBroadcast(getApplicationContext(), REQUEST_CODE, actionIntent, 0);
|
||||
icon = Icon.createWithResource(getApplicationContext(), com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause);
|
||||
remoteAction = new RemoteAction(icon, "pause", "pause the media", pendingIntent);
|
||||
}
|
||||
actions.add(remoteAction);
|
||||
|
||||
|
||||
//add custom actions to pip window
|
||||
PictureInPictureParams params =
|
||||
new PictureInPictureParams.Builder()
|
||||
.setActions(actions)
|
||||
.build();
|
||||
setPictureInPictureParams(params);
|
||||
}
|
||||
|
||||
public void changedToPipMode() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.showControls(false);
|
||||
//create custom actions
|
||||
makePipControls();
|
||||
|
||||
//setup receiver to handle customer actions
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(ACTION_STOP);
|
||||
filter.addAction(ACTION_PAUSE);
|
||||
filter.addAction(ACTION_PLAY);
|
||||
filter.addAction((getString(R.string.app_background_audio)));
|
||||
receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
String action = intent.getAction();
|
||||
assert action != null;
|
||||
if (action.equals(ACTION_PAUSE)) {
|
||||
videoPlayerFragment.pauseVideo();
|
||||
makePipControls();
|
||||
}
|
||||
if (action.equals(ACTION_PLAY)) {
|
||||
videoPlayerFragment.unPauseVideo();
|
||||
makePipControls();
|
||||
}
|
||||
|
||||
if (action.equals(getString(R.string.app_background_audio))) {
|
||||
unregisterReceiver(receiver);
|
||||
finish();
|
||||
}
|
||||
if (action.equals(ACTION_STOP)) {
|
||||
unregisterReceiver(receiver);
|
||||
finishAndRemoveTask();
|
||||
}
|
||||
}
|
||||
};
|
||||
registerReceiver(receiver, filter);
|
||||
|
||||
Log.v(TAG, "switched to pip ");
|
||||
floatMode = true;
|
||||
videoPlayerFragment.showControls(false);
|
||||
}
|
||||
|
||||
public void changedToNormalMode() {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.showControls(true);
|
||||
if (receiver != null) {
|
||||
unregisterReceiver(receiver);
|
||||
}
|
||||
Log.v(TAG, "switched to normal");
|
||||
floatMode = false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
// Set theme
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
setTheme(getResources().getIdentifier(
|
||||
sharedPref.getString(
|
||||
getString(R.string.pref_theme_key),
|
||||
getString(R.string.app_default_theme)
|
||||
),
|
||||
"style",
|
||||
getPackageName())
|
||||
);
|
||||
|
||||
setContentView(R.layout.activity_video_play);
|
||||
|
||||
// get video ID
|
||||
Intent intent = getIntent();
|
||||
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
String playingVideo = videoPlayerFragment.getVideoUuid();
|
||||
Log.v(TAG, "oncreate click: " + videoUuid + " is trying to replace: " + playingVideo);
|
||||
|
||||
if (TextUtils.isEmpty(playingVideo)) {
|
||||
Log.v(TAG, "oncreate no video currently playing");
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else if (!playingVideo.equals(videoUuid)) {
|
||||
Log.v(TAG, "oncreate different video playing currently");
|
||||
videoPlayerFragment.stopVideo();
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else {
|
||||
Log.v(TAG, "oncreate same video playing currently");
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
int orientation = this.getResources().getConfiguration().orientation;
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onNewIntent(Intent intent) {
|
||||
super.onNewIntent(intent);
|
||||
setIntent(intent);
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
assert videoPlayerFragment != null;
|
||||
String videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
|
||||
Log.v(TAG, "new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.getVideoUuid());
|
||||
String playingVideo = videoPlayerFragment.getVideoUuid();
|
||||
|
||||
if (TextUtils.isEmpty(playingVideo)) {
|
||||
Log.v(TAG, "new intent no video currently playing");
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else if (!playingVideo.equals(videoUuid)) {
|
||||
Log.v(TAG, "new intent different video playing currently");
|
||||
videoPlayerFragment.stopVideo();
|
||||
videoPlayerFragment.start(videoUuid);
|
||||
} else {
|
||||
Log.v(TAG, "new intent same video playing currently");
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
int orientation = this.getResources().getConfiguration().orientation;
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onConfigurationChanged(@NonNull Configuration newConfig) {
|
||||
Log.v(TAG, "onConfigurationChanged()...");
|
||||
|
||||
super.onConfigurationChanged(newConfig);
|
||||
|
||||
// Checking the orientation changes of the screen
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true);
|
||||
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
setOrientation(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void setOrientation(Boolean isLandscape) {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
VideoMetaDataFragment videoMetaFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
RelativeLayout.LayoutParams params = (RelativeLayout.LayoutParams) videoPlayerFragment.requireView().getLayoutParams();
|
||||
params.width = FrameLayout.LayoutParams.MATCH_PARENT;
|
||||
params.height = isLandscape ? FrameLayout.LayoutParams.MATCH_PARENT : (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 250, getResources().getDisplayMetrics());
|
||||
|
||||
videoPlayerFragment.requireView().setLayoutParams(params);
|
||||
|
||||
if (videoMetaFragment != null) {
|
||||
FragmentTransaction transaction = fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out);
|
||||
|
||||
if (isLandscape) {
|
||||
transaction.hide(videoMetaFragment);
|
||||
} else {
|
||||
transaction.show(videoMetaFragment);
|
||||
}
|
||||
|
||||
transaction.commit();
|
||||
}
|
||||
|
||||
videoPlayerFragment.setIsFullscreen(isLandscape);
|
||||
|
||||
if ( isLandscape ) {
|
||||
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
} else {
|
||||
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.destroyVideo();
|
||||
|
||||
super.onDestroy();
|
||||
Log.v(TAG, "onDestroy...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPause() {
|
||||
super.onPause();
|
||||
Log.v(TAG, "onPause()...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onResume() {
|
||||
super.onResume();
|
||||
Log.v(TAG, "onResume()...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStop() {
|
||||
super.onStop();
|
||||
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
videoPlayerFragment.stopVideo();
|
||||
|
||||
Log.v(TAG, "onStop()...");
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
Log.v(TAG, "onStart()...");
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
@Override
|
||||
public void onUserLeaveHint() {
|
||||
|
||||
Log.v(TAG, "onUserLeaveHint()...");
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment) fragmentManager.findFragmentById(R.id.video_meta_data_fragment);
|
||||
|
||||
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
assert backgroundBehavior != null;
|
||||
if ( videoMetaDataFragment.isLeaveAppExpected() )
|
||||
{
|
||||
super.onUserLeaveHint();
|
||||
return;
|
||||
}
|
||||
|
||||
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
|
||||
Log.v(TAG, "stop the video");
|
||||
|
||||
videoPlayerFragment.pauseVideo();
|
||||
stopService(new Intent(this, VideoPlayerService.class));
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
|
||||
Log.v(TAG, "play the Audio");
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
|
||||
Log.v(TAG, "play in floating video");
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip");
|
||||
enterPipMode();
|
||||
} else {
|
||||
Log.v(TAG, "unable to use pip");
|
||||
}
|
||||
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback");
|
||||
super.onBackPressed();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
// @RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@SuppressLint("NewApi")
|
||||
public void onBackPressed() {
|
||||
|
||||
Log.v(TAG, "onBackPressed()...");
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment)
|
||||
getSupportFragmentManager().findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
assert videoPlayerFragment != null;
|
||||
|
||||
// copying Youtube behavior to have back button exit full screen.
|
||||
if (videoPlayerFragment.getIsFullscreen()) {
|
||||
Log.v(TAG, "exiting full screen");
|
||||
videoPlayerFragment.fullScreenToggle();
|
||||
return;
|
||||
}
|
||||
// pause video if pref is enabled
|
||||
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
|
||||
videoPlayerFragment.pauseVideo();
|
||||
}
|
||||
|
||||
String backgroundBehavior = sharedPref.getString(getString(R.string.pref_background_behavior_key), getString(R.string.pref_background_stop_key));
|
||||
|
||||
assert backgroundBehavior != null;
|
||||
|
||||
if (backgroundBehavior.equals(getString(R.string.pref_background_stop_key))) {
|
||||
Log.v(TAG, "stop the video");
|
||||
videoPlayerFragment.pauseVideo();
|
||||
stopService(new Intent(this, VideoPlayerService.class));
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_audio_key))) {
|
||||
Log.v(TAG, "play the Audio");
|
||||
super.onBackPressed();
|
||||
|
||||
} else if (backgroundBehavior.equals(getString(R.string.pref_background_float_key))) {
|
||||
Log.v(TAG, "play in floating video");
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip");
|
||||
enterPipMode();
|
||||
//fixes problem where back press doesn't bring up video list after returning from PIP mode
|
||||
Intent intentSettings = new Intent(this, VideoListActivity.class);
|
||||
this.startActivity(intentSettings);
|
||||
} else {
|
||||
Log.v(TAG, "Unable to enter PIP mode");
|
||||
super.onBackPressed();
|
||||
}
|
||||
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback");
|
||||
super.onBackPressed();
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
public void enterPipMode() {
|
||||
final FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
final VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById( R.id.video_player_fragment );
|
||||
|
||||
if ( videoPlayerFragment.getVideoAspectRatio() == 0 ) {
|
||||
Log.i( TAG, "impossible to switch to pip" );
|
||||
} else {
|
||||
Rational rational = new Rational( (int) ( videoPlayerFragment.getVideoAspectRatio() * 100 ), 100 );
|
||||
PictureInPictureParams mParams =
|
||||
new PictureInPictureParams.Builder()
|
||||
.setAspectRatio( rational )
|
||||
// .setSourceRectHint(new Rect(0,500,400,600))
|
||||
.build();
|
||||
|
||||
enterPictureInPictureMode( mParams );
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPictureInPictureModeChanged(boolean isInPictureInPictureMode, Configuration newConfig) {
|
||||
FragmentManager fragmentManager = getSupportFragmentManager();
|
||||
VideoPlayerFragment videoPlayerFragment = (VideoPlayerFragment) fragmentManager.findFragmentById(R.id.video_player_fragment);
|
||||
|
||||
if (videoPlayerFragment != null) {
|
||||
|
||||
if (isInPictureInPictureMode) {
|
||||
changedToPipMode();
|
||||
Log.v(TAG, "switched to pip ");
|
||||
videoPlayerFragment.useController(false);
|
||||
} else {
|
||||
changedToNormalMode();
|
||||
Log.v(TAG, "switched to normal");
|
||||
videoPlayerFragment.useController(true);
|
||||
}
|
||||
|
||||
} else {
|
||||
Log.e(TAG, "videoPlayerFragment is NULL");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,461 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.activity
|
||||
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import android.annotation.SuppressLint
|
||||
import net.schueller.peertube.fragment.VideoPlayerFragment
|
||||
import net.schueller.peertube.R
|
||||
import android.app.RemoteAction
|
||||
import android.app.PendingIntent
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.*
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.Icon
|
||||
import android.os.Bundle
|
||||
import android.text.TextUtils
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment
|
||||
import android.widget.RelativeLayout
|
||||
import android.widget.FrameLayout
|
||||
import android.util.TypedValue
|
||||
import android.view.WindowManager
|
||||
import net.schueller.peertube.service.VideoPlayerService
|
||||
import net.schueller.peertube.helper.VideoHelper
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import android.util.Rational
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.schueller.peertube.fragment.VideoDescriptionFragment
|
||||
import java.util.ArrayList
|
||||
|
||||
class VideoPlayActivity : CommonActivity() {
|
||||
private var receiver: BroadcastReceiver? = null
|
||||
|
||||
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
|
||||
@SuppressLint("NewApi")
|
||||
fun makePipControls() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
val actions = ArrayList<RemoteAction>()
|
||||
var actionIntent = Intent(getString(R.string.app_background_audio))
|
||||
var pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
@SuppressLint("NewApi", "LocalSuppress") var icon = Icon.createWithResource(
|
||||
applicationContext, android.R.drawable.stat_sys_speakerphone
|
||||
)
|
||||
@SuppressLint("NewApi", "LocalSuppress") var remoteAction =
|
||||
RemoteAction(icon!!, "close pip", "from pip window custom command", pendingIntent!!)
|
||||
actions.add(remoteAction)
|
||||
actionIntent = Intent(PlayerNotificationManager.ACTION_STOP)
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
icon = Icon.createWithResource(
|
||||
applicationContext,
|
||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop
|
||||
)
|
||||
remoteAction = RemoteAction(icon, "play", "stop the media", pendingIntent)
|
||||
actions.add(remoteAction)
|
||||
assert(videoPlayerFragment != null)
|
||||
if (videoPlayerFragment!!.isPaused) {
|
||||
Log.e(TAG, "setting actions with play button")
|
||||
actionIntent = Intent(PlayerNotificationManager.ACTION_PLAY)
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
icon = Icon.createWithResource(
|
||||
applicationContext,
|
||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play
|
||||
)
|
||||
remoteAction = RemoteAction(icon, "play", "play the media", pendingIntent)
|
||||
} else {
|
||||
Log.e(TAG, "setting actions with pause button")
|
||||
actionIntent = Intent(PlayerNotificationManager.ACTION_PAUSE)
|
||||
pendingIntent =
|
||||
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
|
||||
icon = Icon.createWithResource(
|
||||
applicationContext,
|
||||
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause
|
||||
)
|
||||
remoteAction = RemoteAction(icon, "pause", "pause the media", pendingIntent)
|
||||
}
|
||||
actions.add(remoteAction)
|
||||
|
||||
|
||||
//add custom actions to pip window
|
||||
val params = PictureInPictureParams.Builder()
|
||||
.setActions(actions)
|
||||
.build()
|
||||
setPictureInPictureParams(params)
|
||||
}
|
||||
|
||||
private fun changedToPipMode() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.showControls(false)
|
||||
//create custom actions
|
||||
makePipControls()
|
||||
|
||||
//setup receiver to handle customer actions
|
||||
val filter = IntentFilter()
|
||||
filter.addAction(PlayerNotificationManager.ACTION_STOP)
|
||||
filter.addAction(PlayerNotificationManager.ACTION_PAUSE)
|
||||
filter.addAction(PlayerNotificationManager.ACTION_PLAY)
|
||||
filter.addAction(getString(R.string.app_background_audio))
|
||||
receiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val action = intent.action!!
|
||||
if (action == PlayerNotificationManager.ACTION_PAUSE) {
|
||||
videoPlayerFragment.pauseVideo()
|
||||
makePipControls()
|
||||
}
|
||||
if (action == PlayerNotificationManager.ACTION_PLAY) {
|
||||
videoPlayerFragment.unPauseVideo()
|
||||
makePipControls()
|
||||
}
|
||||
if (action == getString(R.string.app_background_audio)) {
|
||||
unregisterReceiver(receiver)
|
||||
finish()
|
||||
}
|
||||
if (action == PlayerNotificationManager.ACTION_STOP) {
|
||||
unregisterReceiver(receiver)
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
}
|
||||
}
|
||||
registerReceiver(receiver, filter)
|
||||
Log.v(TAG, "switched to pip ")
|
||||
floatMode = true
|
||||
videoPlayerFragment.showControls(false)
|
||||
}
|
||||
|
||||
private fun changedToNormalMode() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.showControls(true)
|
||||
if (receiver != null) {
|
||||
unregisterReceiver(receiver)
|
||||
}
|
||||
Log.v(TAG, "switched to normal")
|
||||
floatMode = false
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
// Set theme
|
||||
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
setTheme(
|
||||
resources.getIdentifier(
|
||||
sharedPref.getString(
|
||||
getString(R.string.pref_theme_key),
|
||||
getString(R.string.app_default_theme)
|
||||
),
|
||||
"style",
|
||||
packageName
|
||||
)
|
||||
)
|
||||
setContentView(R.layout.activity_video_play)
|
||||
|
||||
// get video ID
|
||||
val intent = intent
|
||||
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
val playingVideo = videoPlayerFragment.videoUuid
|
||||
Log.v(TAG, "oncreate click: $videoUuid is trying to replace: $playingVideo")
|
||||
when {
|
||||
TextUtils.isEmpty(playingVideo) -> {
|
||||
Log.v(TAG, "oncreate no video currently playing")
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
playingVideo != videoUuid -> {
|
||||
Log.v(TAG, "oncreate different video playing currently")
|
||||
videoPlayerFragment.stopVideo()
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
else -> {
|
||||
Log.v(TAG, "oncreate same video playing currently")
|
||||
}
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
val orientation = this.resources.configuration.orientation
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
setIntent(intent)
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
|
||||
Log.v(
|
||||
TAG,
|
||||
"new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.videoUuid
|
||||
)
|
||||
val playingVideo = videoPlayerFragment.videoUuid
|
||||
when {
|
||||
TextUtils.isEmpty(playingVideo) -> {
|
||||
Log.v(TAG, "new intent no video currently playing")
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
playingVideo != videoUuid -> {
|
||||
Log.v(TAG, "new intent different video playing currently")
|
||||
videoPlayerFragment.stopVideo()
|
||||
videoPlayerFragment.start(videoUuid)
|
||||
}
|
||||
else -> {
|
||||
Log.v(TAG, "new intent same video playing currently")
|
||||
}
|
||||
}
|
||||
|
||||
// if we are in landscape set the video to fullscreen
|
||||
val orientation = this.resources.configuration.orientation
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
Log.v(TAG, "onConfigurationChanged()...")
|
||||
super.onConfigurationChanged(newConfig)
|
||||
|
||||
// Checking the orientation changes of the screen
|
||||
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
setOrientation(true)
|
||||
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
setOrientation(false)
|
||||
}
|
||||
}
|
||||
|
||||
private fun setOrientation(isLandscape: Boolean) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
val videoMetaFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
|
||||
assert(videoPlayerFragment != null)
|
||||
val params = videoPlayerFragment!!.requireView().layoutParams as RelativeLayout.LayoutParams
|
||||
params.width = FrameLayout.LayoutParams.MATCH_PARENT
|
||||
params.height =
|
||||
if (isLandscape) FrameLayout.LayoutParams.MATCH_PARENT else TypedValue.applyDimension(
|
||||
TypedValue.COMPLEX_UNIT_DIP,
|
||||
250f,
|
||||
resources.displayMetrics
|
||||
)
|
||||
.toInt()
|
||||
videoPlayerFragment.requireView().layoutParams = params
|
||||
if (videoMetaFragment != null) {
|
||||
val transaction = fragmentManager.beginTransaction()
|
||||
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
|
||||
if (isLandscape) {
|
||||
transaction.hide(videoMetaFragment)
|
||||
} else {
|
||||
transaction.show(videoMetaFragment)
|
||||
}
|
||||
transaction.commit()
|
||||
}
|
||||
videoPlayerFragment.setIsFullscreen(isLandscape)
|
||||
if (isLandscape) {
|
||||
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
} else {
|
||||
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.destroyVideo()
|
||||
super.onDestroy()
|
||||
Log.v(TAG, "onDestroy...")
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
super.onPause()
|
||||
Log.v(TAG, "onPause()...")
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
Log.v(TAG, "onResume()...")
|
||||
}
|
||||
|
||||
override fun onStop() {
|
||||
super.onStop()
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
videoPlayerFragment.stopVideo()
|
||||
|
||||
// TODO: doesn't remove fragment??
|
||||
val fragment: Fragment? = supportFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
|
||||
if (fragment != null) {
|
||||
Log.v(TAG, "remove VideoDescriptionFragment")
|
||||
supportFragmentManager.beginTransaction().remove(fragment).commit()
|
||||
}
|
||||
|
||||
Log.v(TAG, "onStop()...")
|
||||
}
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
Log.v(TAG, "onStart()...")
|
||||
}
|
||||
|
||||
@SuppressLint("NewApi")
|
||||
public override fun onUserLeaveHint() {
|
||||
Log.v(TAG, "onUserLeaveHint()...")
|
||||
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
val videoMetaDataFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
|
||||
val backgroundBehavior = sharedPref.getString(
|
||||
getString(R.string.pref_background_behavior_key),
|
||||
getString(R.string.pref_background_stop_key)
|
||||
)
|
||||
assert(videoPlayerFragment != null)
|
||||
assert(backgroundBehavior != null)
|
||||
if (videoMetaDataFragment!!.isLeaveAppExpected) {
|
||||
super.onUserLeaveHint()
|
||||
return
|
||||
}
|
||||
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
|
||||
Log.v(TAG, "stop the video")
|
||||
videoPlayerFragment!!.pauseVideo()
|
||||
stopService(Intent(this, VideoPlayerService::class.java))
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
|
||||
Log.v(TAG, "play the Audio")
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
|
||||
Log.v(TAG, "play in floating video")
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (VideoHelper.canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip")
|
||||
enterPipMode()
|
||||
} else {
|
||||
Log.v(TAG, "unable to use pip")
|
||||
}
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback")
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
// @RequiresApi(api = Build.VERSION_CODES.O)
|
||||
@SuppressLint("NewApi")
|
||||
override fun onBackPressed() {
|
||||
Log.v(TAG, "onBackPressed()...")
|
||||
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
|
||||
val videoPlayerFragment =
|
||||
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
|
||||
|
||||
// copying Youtube behavior to have back button exit full screen.
|
||||
if (videoPlayerFragment.getIsFullscreen()) {
|
||||
Log.v(TAG, "exiting full screen")
|
||||
videoPlayerFragment.fullScreenToggle()
|
||||
return
|
||||
}
|
||||
// pause video if pref is enabled
|
||||
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
|
||||
videoPlayerFragment.pauseVideo()
|
||||
}
|
||||
val backgroundBehavior = sharedPref.getString(
|
||||
getString(R.string.pref_background_behavior_key),
|
||||
getString(R.string.pref_background_stop_key)
|
||||
)!!
|
||||
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
|
||||
Log.v(TAG, "stop the video")
|
||||
videoPlayerFragment.pauseVideo()
|
||||
stopService(Intent(this, VideoPlayerService::class.java))
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
|
||||
Log.v(TAG, "play the Audio")
|
||||
super.onBackPressed()
|
||||
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
|
||||
Log.v(TAG, "play in floating video")
|
||||
//canEnterPIPMode makes sure API level is high enough
|
||||
if (VideoHelper.canEnterPipMode(this)) {
|
||||
Log.v(TAG, "enabling pip")
|
||||
enterPipMode()
|
||||
//fixes problem where back press doesn't bring up video list after returning from PIP mode
|
||||
val intentSettings = Intent(this, VideoListActivity::class.java)
|
||||
this.startActivity(intentSettings)
|
||||
} else {
|
||||
Log.v(TAG, "Unable to enter PIP mode")
|
||||
super.onBackPressed()
|
||||
}
|
||||
} else {
|
||||
// Deal with bad entries from older version
|
||||
Log.v(TAG, "No setting, fallback")
|
||||
super.onBackPressed()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(api = Build.VERSION_CODES.O)
|
||||
fun enterPipMode() {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
if (videoPlayerFragment!!.videoAspectRatio == 0.toFloat()) {
|
||||
Log.i(TAG, "impossible to switch to pip")
|
||||
} else {
|
||||
val rational = Rational((videoPlayerFragment.videoAspectRatio * 100).toInt(), 100)
|
||||
val mParams = PictureInPictureParams.Builder()
|
||||
.setAspectRatio(rational) // .setSourceRectHint(new Rect(0,500,400,600))
|
||||
.build()
|
||||
enterPictureInPictureMode(mParams)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onPictureInPictureModeChanged(
|
||||
isInPictureInPictureMode: Boolean,
|
||||
newConfig: Configuration
|
||||
) {
|
||||
val fragmentManager = supportFragmentManager
|
||||
val videoPlayerFragment =
|
||||
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
|
||||
if (videoPlayerFragment != null) {
|
||||
if (isInPictureInPictureMode) {
|
||||
changedToPipMode()
|
||||
Log.v(TAG, "switched to pip ")
|
||||
videoPlayerFragment.useController(false)
|
||||
} else {
|
||||
changedToNormalMode()
|
||||
Log.v(TAG, "switched to normal")
|
||||
videoPlayerFragment.useController(true)
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "videoPlayerFragment is NULL")
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "VideoPlayActivity"
|
||||
var floatMode = false
|
||||
private const val REQUEST_CODE = 101
|
||||
}
|
||||
}
|
@ -4,19 +4,14 @@ import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
|
||||
import net.schueller.peertube.databinding.ItemChannelTitleBinding
|
||||
import net.schueller.peertube.databinding.ItemTagTitleBinding
|
||||
import net.schueller.peertube.databinding.RowVideoListBinding
|
||||
import net.schueller.peertube.model.Category
|
||||
import net.schueller.peertube.model.Channel
|
||||
import net.schueller.peertube.model.TagVideo
|
||||
import net.schueller.peertube.model.Video
|
||||
import net.schueller.peertube.model.VideoList
|
||||
import net.schueller.peertube.databinding.*
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment
|
||||
import net.schueller.peertube.model.*
|
||||
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
|
||||
import net.schueller.peertube.model.ui.VideoMetaViewItem
|
||||
import java.util.ArrayList
|
||||
|
||||
class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
|
||||
class MultiViewRecycleViewAdapter(private val videoMetaDataFragment: VideoMetaDataFragment? = null) : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
|
||||
|
||||
private var items = ArrayList<OverviewRecycleViewItem>()
|
||||
set(value) {
|
||||
@ -34,6 +29,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setVideoMeta(videoMetaViewItem: VideoMetaViewItem) {
|
||||
items.add(videoMetaViewItem)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setCategoryTitle(category: Category) {
|
||||
items.add(category)
|
||||
notifyDataSetChanged()
|
||||
@ -49,6 +49,11 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun setVideoComment(commentThread: CommentThread) {
|
||||
items.add(commentThread)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun clearData() {
|
||||
items.clear()
|
||||
notifyDataSetChanged()
|
||||
@ -83,6 +88,21 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
||||
false
|
||||
)
|
||||
)
|
||||
R.layout.item_video_meta -> MultiViewRecyclerViewHolder.VideoMetaViewHolder(
|
||||
ItemVideoMetaBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
),
|
||||
videoMetaDataFragment
|
||||
)
|
||||
R.layout.item_video_comments_overview -> MultiViewRecyclerViewHolder.VideoCommentsViewHolder(
|
||||
ItemVideoCommentsOverviewBinding.inflate(
|
||||
LayoutInflater.from(parent.context),
|
||||
parent,
|
||||
false
|
||||
)
|
||||
)
|
||||
else -> throw IllegalArgumentException("Invalid ViewType Provided")
|
||||
}
|
||||
}
|
||||
@ -93,6 +113,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
||||
is MultiViewRecyclerViewHolder.CategoryViewHolder -> holder.bind(items[position] as Category)
|
||||
is MultiViewRecyclerViewHolder.ChannelViewHolder -> holder.bind(items[position] as Channel)
|
||||
is MultiViewRecyclerViewHolder.TagViewHolder -> holder.bind(items[position] as TagVideo)
|
||||
is MultiViewRecyclerViewHolder.VideoMetaViewHolder -> holder.bind(items[position] as VideoMetaViewItem)
|
||||
is MultiViewRecyclerViewHolder.VideoCommentsViewHolder -> holder.bind(items[position] as CommentThread)
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,6 +126,8 @@ class MultiViewRecycleViewAdapter : RecyclerView.Adapter<MultiViewRecyclerViewHo
|
||||
is Channel -> R.layout.item_channel_title
|
||||
is Category -> R.layout.item_category_title
|
||||
is TagVideo -> R.layout.item_tag_title
|
||||
is VideoMetaViewItem -> R.layout.item_video_meta
|
||||
is CommentThread -> R.layout.item_video_comments_overview
|
||||
else -> { return 0}
|
||||
}
|
||||
}
|
||||
|
@ -16,47 +16,286 @@
|
||||
*/
|
||||
package net.schueller.peertube.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.util.Log
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.view.View.GONE
|
||||
import android.widget.Toast
|
||||
import androidx.appcompat.widget.PopupMenu
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import com.google.gson.JsonObject
|
||||
import com.squareup.picasso.Picasso
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.R.color
|
||||
import net.schueller.peertube.R.string
|
||||
import net.schueller.peertube.activity.AccountActivity
|
||||
import net.schueller.peertube.activity.VideoListActivity
|
||||
import net.schueller.peertube.activity.VideoListActivity.Companion
|
||||
import net.schueller.peertube.activity.VideoPlayActivity
|
||||
import net.schueller.peertube.databinding.ItemCategoryTitleBinding
|
||||
import net.schueller.peertube.databinding.ItemChannelTitleBinding
|
||||
import net.schueller.peertube.databinding.RowVideoListBinding
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getDuration
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
|
||||
import net.schueller.peertube.model.Avatar
|
||||
import net.schueller.peertube.model.Category
|
||||
import net.schueller.peertube.model.Channel
|
||||
import net.schueller.peertube.model.Video
|
||||
import com.mikepenz.iconics.Iconics.Builder
|
||||
import net.schueller.peertube.R.id
|
||||
import net.schueller.peertube.R.menu
|
||||
import net.schueller.peertube.databinding.ItemTagTitleBinding
|
||||
import net.schueller.peertube.databinding.*
|
||||
import net.schueller.peertube.fragment.VideoMetaDataFragment
|
||||
import net.schueller.peertube.intents.Intents
|
||||
import net.schueller.peertube.model.TagVideo
|
||||
import net.schueller.peertube.model.*
|
||||
import net.schueller.peertube.model.ui.VideoMetaViewItem
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import net.schueller.peertube.network.Session
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.network.GetUserService
|
||||
|
||||
|
||||
sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.ViewHolder(binding.root) {
|
||||
|
||||
var videoRating: Rating? = null
|
||||
var isLeaveAppExpected = false
|
||||
|
||||
class CategoryViewHolder(private val binding: ItemCategoryTitleBinding): MultiViewRecyclerViewHolder(binding) {
|
||||
fun bind(category: Category) {
|
||||
binding.textViewTitle.text = category.label
|
||||
}
|
||||
}
|
||||
|
||||
class VideoCommentsViewHolder(private val binding: ItemVideoCommentsOverviewBinding): MultiViewRecyclerViewHolder(binding) {
|
||||
fun bind(commentThread: CommentThread) {
|
||||
|
||||
binding.videoCommentsTotalCount.text = commentThread.total.toString()
|
||||
|
||||
if (commentThread.comments.isNotEmpty()) {
|
||||
val highlightedComment: Comment = commentThread.comments[0]
|
||||
|
||||
// owner / creator Avatar
|
||||
val avatar = highlightedComment.account.avatar
|
||||
if (avatar != null) {
|
||||
val baseUrl = APIUrlHelper.getUrl(binding.videoHighlightedAvatar.context)
|
||||
val avatarPath = avatar.path
|
||||
Picasso.get()
|
||||
.load(baseUrl + avatarPath)
|
||||
.into(binding.videoHighlightedAvatar)
|
||||
}
|
||||
binding.videoHighlightedComment.text = highlightedComment.text
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class VideoMetaViewHolder(private val binding: ItemVideoMetaBinding, private val videoMetaDataFragment: VideoMetaDataFragment?): MultiViewRecyclerViewHolder(binding) {
|
||||
fun bind(videoMetaViewItem: VideoMetaViewItem) {
|
||||
|
||||
val video = videoMetaViewItem.video
|
||||
|
||||
if (video != null && videoMetaDataFragment != null) {
|
||||
|
||||
val context = binding.avatar.context
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val userService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetUserService::class.java
|
||||
)
|
||||
|
||||
// Title
|
||||
binding.videoName.text = video.name
|
||||
binding.videoOpenDescription.setOnClickListener {
|
||||
videoMetaDataFragment.showDescriptionFragment(video)
|
||||
}
|
||||
|
||||
// Thumbs up
|
||||
binding.videoThumbsUpWrapper.setOnClickListener {
|
||||
rateVideo(true, video, context, binding)
|
||||
}
|
||||
|
||||
// Thumbs Down
|
||||
binding.videoThumbsDownWrapper.setOnClickListener {
|
||||
rateVideo(false, video, context, binding)
|
||||
}
|
||||
|
||||
binding.videoAddToPlaylistWrapper.setOnClickListener {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_feature_not_yet_implemented),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
binding.videoBlockWrapper.setOnClickListener {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_feature_not_yet_implemented),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
binding.videoFlagWrapper.setOnClickListener {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_feature_not_yet_implemented),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
|
||||
// video rating
|
||||
videoRating = Rating()
|
||||
videoRating!!.rating = RATING_NONE // default
|
||||
updateVideoRating(video, binding)
|
||||
|
||||
// Retrieve which rating the user gave to this video
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
val call = videoDataService.getVideoRating(video.id)
|
||||
call.enqueue(object : Callback<Rating?> {
|
||||
override fun onResponse(call: Call<Rating?>, response: Response<Rating?>) {
|
||||
videoRating = response.body()
|
||||
updateVideoRating(video, binding)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Rating?>, t: Throwable) {
|
||||
// Do nothing.
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Share
|
||||
binding.videoShare.setOnClickListener {
|
||||
isLeaveAppExpected = true
|
||||
Intents.Share(context, video)
|
||||
}
|
||||
|
||||
// hide download if not supported by video
|
||||
if (video.downloadEnabled) {
|
||||
binding.videoDownloadWrapper.setOnClickListener {
|
||||
Intents.Download(context, video)
|
||||
}
|
||||
} else {
|
||||
binding.videoDownloadWrapper.visibility = GONE
|
||||
}
|
||||
|
||||
val account = video.account
|
||||
|
||||
// owner / creator Avatar
|
||||
val avatar = account.avatar
|
||||
if (avatar != null) {
|
||||
val baseUrl = APIUrlHelper.getUrl(context)
|
||||
val avatarPath = avatar.path
|
||||
Picasso.get()
|
||||
.load(baseUrl + avatarPath)
|
||||
.into(binding.avatar)
|
||||
}
|
||||
// created at / views
|
||||
binding.videoMeta.text = getMetaString(
|
||||
video.createdAt,
|
||||
video.views,
|
||||
context!!
|
||||
)
|
||||
|
||||
// owner / creator
|
||||
binding.videoOwner.text = getOwnerString(
|
||||
video.account.name,
|
||||
video.account.host,
|
||||
context
|
||||
)
|
||||
|
||||
// videoOwnerSubscribers
|
||||
binding.videoOwnerSubscribers.text = video.account.followersCount.toString()
|
||||
|
||||
|
||||
// get subscription status
|
||||
var isSubscribed = false
|
||||
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
val subChannel = video.channel.name + "@" + video.channel.host
|
||||
val call = userService.subscriptionsExist(subChannel)
|
||||
call.enqueue(object : Callback<JsonObject> {
|
||||
override fun onResponse(call: Call<JsonObject>, response: Response<JsonObject>) {
|
||||
if (response.isSuccessful) {
|
||||
// {"video.channel.name + "@" + video.channel.host":true}
|
||||
if (response.body()?.get(video.channel.name + "@" + video.channel.host)!!.asBoolean) {
|
||||
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
|
||||
isSubscribed = true;
|
||||
} else {
|
||||
binding.videoOwnerSubscribeButton.setText(string.subscribe)
|
||||
}
|
||||
}
|
||||
}
|
||||
override fun onFailure(call: Call<JsonObject>, t: Throwable) {
|
||||
// Do nothing.
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
binding.videoOwnerSubscribeButton.setOnClickListener {
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
if (!isSubscribed) {
|
||||
val payload = video.channel.name + "@" + video.channel.host
|
||||
val body = "{\"uri\":\"$payload\"}".toRequestBody("application/json".toMediaType())
|
||||
val call = userService.subscribe(body)
|
||||
call.enqueue(object : Callback<ResponseBody?> {
|
||||
override fun onResponse(
|
||||
call: Call<ResponseBody?>,
|
||||
response: Response<ResponseBody?>
|
||||
) {
|
||||
if (response.isSuccessful) {
|
||||
binding.videoOwnerSubscribeButton.setText(string.unsubscribe)
|
||||
isSubscribed = true
|
||||
}
|
||||
}
|
||||
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
|
||||
// Do nothing.
|
||||
}
|
||||
})
|
||||
} else {
|
||||
val payload = video.channel.name + "@" + video.channel.host
|
||||
val call = userService.unsubscribe(payload)
|
||||
call.enqueue(object : Callback<ResponseBody?> {
|
||||
override fun onResponse(
|
||||
call: Call<ResponseBody?>,
|
||||
response: Response<ResponseBody?>
|
||||
) {
|
||||
if (response.isSuccessful) {
|
||||
binding.videoOwnerSubscribeButton.setText(string.subscribe)
|
||||
isSubscribed = false
|
||||
}
|
||||
}
|
||||
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
|
||||
// Do nothing.
|
||||
}
|
||||
})
|
||||
}
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_login_required_for_service),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
class ChannelViewHolder(private val binding: ItemChannelTitleBinding): MultiViewRecyclerViewHolder(binding) {
|
||||
fun bind(channel: Channel) {
|
||||
|
||||
@ -178,4 +417,117 @@ sealed class MultiViewRecyclerViewHolder(binding: ViewBinding) : RecyclerView.Vi
|
||||
}
|
||||
|
||||
|
||||
fun updateVideoRating(video: Video?, binding: ItemVideoMetaBinding) {
|
||||
|
||||
when (videoRating!!.rating) {
|
||||
RATING_NONE -> {
|
||||
Log.v("MWCVH", "RATING_NONE")
|
||||
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
|
||||
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
|
||||
}
|
||||
RATING_LIKE -> {
|
||||
Log.v("MWCVH", "RATING_LIKE")
|
||||
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up_filled)
|
||||
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
|
||||
}
|
||||
RATING_DISLIKE -> {
|
||||
Log.v("MWCVH", "RATING_DISLIKE")
|
||||
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
|
||||
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down_filled)
|
||||
}
|
||||
}
|
||||
|
||||
// Update the texts
|
||||
binding.videoThumbsUpTotal.text = video?.likes.toString()
|
||||
binding.videoThumbsDownTotal.text = video?.dislikes.toString()
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: move this out and get update when rating changes
|
||||
*/
|
||||
fun rateVideo(like: Boolean, video: Video, context: Context, binding: ItemVideoMetaBinding) {
|
||||
if (Session.getInstance().isLoggedIn) {
|
||||
val ratePayload: String = when (videoRating!!.rating) {
|
||||
RATING_LIKE -> if (like) RATING_NONE else RATING_DISLIKE
|
||||
RATING_DISLIKE -> if (like) RATING_LIKE else RATING_NONE
|
||||
RATING_NONE -> if (like) RATING_LIKE else RATING_DISLIKE
|
||||
else -> if (like) RATING_LIKE else RATING_DISLIKE
|
||||
}
|
||||
|
||||
val body = "{\"rating\":\"$ratePayload\"}".toRequestBody("application/json".toMediaType())
|
||||
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL, APIUrlHelper.useInsecureConnection(
|
||||
context
|
||||
)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val call = videoDataService.rateVideo(video.id, body)
|
||||
call.enqueue(object : Callback<ResponseBody?> {
|
||||
override fun onResponse(
|
||||
call: Call<ResponseBody?>,
|
||||
response: Response<ResponseBody?>
|
||||
) {
|
||||
// if 20x, update likes/dislikes
|
||||
if (response.isSuccessful) {
|
||||
val previousRating = videoRating!!.rating
|
||||
|
||||
// Update the likes/dislikes count of the video, if needed.
|
||||
// This is only a visual trick, as the actual like/dislike count has
|
||||
// already been modified on the PeerTube instance.
|
||||
if (previousRating != ratePayload) {
|
||||
when (previousRating) {
|
||||
RATING_NONE -> if (ratePayload == RATING_LIKE) {
|
||||
video.likes = video.likes + 1
|
||||
} else {
|
||||
video.dislikes = video.dislikes + 1
|
||||
}
|
||||
RATING_LIKE -> {
|
||||
video.likes = video.likes - 1
|
||||
if (ratePayload == RATING_DISLIKE) {
|
||||
video.dislikes = video.dislikes + 1
|
||||
}
|
||||
}
|
||||
RATING_DISLIKE -> {
|
||||
video.dislikes = video.dislikes - 1
|
||||
if (ratePayload == RATING_LIKE) {
|
||||
video.likes = video.likes + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
videoRating!!.rating = ratePayload
|
||||
updateVideoRating(video, binding)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_rating_failed),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Toast.makeText(
|
||||
context,
|
||||
context.getString(string.video_login_required_for_service),
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val RATING_NONE = "none"
|
||||
private const val RATING_LIKE = "like"
|
||||
private const val RATING_DISLIKE = "dislike"
|
||||
const val EXTRA_VIDEOID = "VIDEOID"
|
||||
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ import android.os.Parcelable
|
||||
import androidx.room.PrimaryKey
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import kotlinx.android.parcel.Parcelize
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "server_table")
|
||||
|
@ -17,7 +17,6 @@
|
||||
package net.schueller.peertube.database
|
||||
|
||||
import android.app.Application
|
||||
import android.os.AsyncTask
|
||||
import androidx.lifecycle.LiveData
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
|
@ -19,12 +19,13 @@ package net.schueller.peertube.fragment
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.util.Patterns
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.Toast
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import net.schueller.peertube.R
|
||||
@ -52,7 +53,7 @@ class AddServerFragment : Fragment() {
|
||||
}
|
||||
|
||||
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
|
||||
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
|
||||
mBinding = FragmentAddServerBinding.inflate(inflater, container, false)
|
||||
return mBinding.root
|
||||
}
|
||||
@ -115,7 +116,7 @@ class AddServerFragment : Fragment() {
|
||||
|
||||
mBinding.pickServerUrl.setOnClickListener {
|
||||
val intentServer = Intent(activity, SearchServerActivity::class.java)
|
||||
this.startActivityForResult(intentServer, PICK_SERVER)
|
||||
openActivityForResult(intentServer)
|
||||
}
|
||||
}
|
||||
|
||||
@ -132,35 +133,24 @@ class AddServerFragment : Fragment() {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
|
||||
if (requestCode != PICK_SERVER) {
|
||||
return
|
||||
}
|
||||
|
||||
if (resultCode != Activity.RESULT_OK) {
|
||||
return
|
||||
}
|
||||
|
||||
val serverUrlTest = data?.getStringExtra("serverUrl")
|
||||
//Log.d(TAG, "serverUrl " + serverUrlTest);
|
||||
|
||||
mBinding.serverUrl.setText(serverUrlTest)
|
||||
|
||||
mBinding.serverLabel.apply {
|
||||
if (text.toString().isBlank()) {
|
||||
setText(data?.getStringExtra("serverName"))
|
||||
private var resultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val intent = result.data
|
||||
val serverUrlTest = intent?.getStringExtra("serverUrl")
|
||||
mBinding.serverUrl.setText(serverUrlTest)
|
||||
mBinding.serverLabel.apply {
|
||||
if (text.toString().isBlank()) {
|
||||
setText(intent?.getStringExtra("serverName"))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun openActivityForResult(intent: Intent) {
|
||||
resultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "AddServerFragment"
|
||||
private const val PICK_SERVER = 1
|
||||
|
||||
private const val SERVER_ARG = "server"
|
||||
|
||||
fun newInstance(server: Server) = AddServerFragment().apply {
|
||||
|
@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.fragment
|
||||
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ImageButton
|
||||
import net.schueller.peertube.R
|
||||
import android.widget.TextView
|
||||
import androidx.fragment.app.Fragment
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.helper.ErrorHelper
|
||||
import net.schueller.peertube.model.Description
|
||||
import net.schueller.peertube.model.Video
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
|
||||
class VideoDescriptionFragment : Fragment () {
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater,
|
||||
container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
val view = inflater.inflate(
|
||||
R.layout.fragment_video_description, container,
|
||||
false
|
||||
)
|
||||
|
||||
val video = video
|
||||
|
||||
if (video != null) {
|
||||
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
|
||||
// description, get extended if available
|
||||
val videoDescription = view.findViewById<TextView>(R.id.description)
|
||||
val shortDescription = video.description
|
||||
if (shortDescription != null && shortDescription.length > 237) {
|
||||
val call = videoDataService.getVideoFullDescription(video.uuid);
|
||||
call.enqueue(object : Callback<Description?> {
|
||||
override fun onResponse(call: Call<Description?>, response: Response<Description?>) {
|
||||
val videoFullDescription: Description? = response.body();
|
||||
|
||||
videoDescription.text = videoFullDescription?.description
|
||||
}
|
||||
override fun onFailure(call: Call<Description?>, t: Throwable) {
|
||||
Log.wtf(TAG, t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(activity, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
videoDescription.text = shortDescription;
|
||||
|
||||
val closeButton = view.findViewById<ImageButton>(R.id.video_description_close_button)
|
||||
closeButton.setOnClickListener {
|
||||
videoMetaDataFragment!!.hideDescriptionFragment()
|
||||
}
|
||||
|
||||
// video privacy
|
||||
val videoPrivacy = view.findViewById<TextView>(R.id.video_privacy);
|
||||
videoPrivacy.text = video!!.privacy.label;
|
||||
|
||||
// video category
|
||||
val videoCategory = view.findViewById<TextView>(R.id.video_category);
|
||||
videoCategory.text = video!!.category.label;
|
||||
|
||||
// video privacy
|
||||
val videoLicense = view.findViewById<TextView>(R.id.video_license);
|
||||
videoLicense.text = video!!.licence.label;
|
||||
|
||||
// video language
|
||||
val videoLanguage = view.findViewById<TextView>(R.id.video_language);
|
||||
videoLanguage.text = video!!.language.label;
|
||||
|
||||
// video privacy
|
||||
val videoTags = view.findViewById<TextView>(R.id.video_tags);
|
||||
videoTags.text = android.text.TextUtils.join(", ", video!!.tags);
|
||||
}
|
||||
|
||||
|
||||
return view
|
||||
}
|
||||
|
||||
companion object {
|
||||
private var video: Video? = null
|
||||
private var videoMetaDataFragment: VideoMetaDataFragment? = null
|
||||
const val TAG = "VideoDescr"
|
||||
fun newInstance(mVideo: Video?, mVideoMetaDataFragment: VideoMetaDataFragment): VideoDescriptionFragment {
|
||||
video = mVideo
|
||||
videoMetaDataFragment = mVideoMetaDataFragment
|
||||
return VideoDescriptionFragment()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,408 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.fragment;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.content.res.TypedArray;
|
||||
import android.os.Bundle;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
import com.squareup.picasso.Picasso;
|
||||
|
||||
import java.util.Objects;
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.helper.ErrorHelper;
|
||||
import net.schueller.peertube.helper.MetaDataHelper;
|
||||
import net.schueller.peertube.intents.Intents;
|
||||
import net.schueller.peertube.model.Account;
|
||||
import net.schueller.peertube.model.Avatar;
|
||||
import net.schueller.peertube.model.Description;
|
||||
import net.schueller.peertube.model.Rating;
|
||||
import net.schueller.peertube.model.Video;
|
||||
import net.schueller.peertube.network.GetVideoDataService;
|
||||
import net.schueller.peertube.network.RetrofitInstance;
|
||||
import net.schueller.peertube.network.Session;
|
||||
import net.schueller.peertube.service.VideoPlayerService;
|
||||
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.core.app.ActivityCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
public class VideoMetaDataFragment extends Fragment {
|
||||
|
||||
private static final String TAG = "VideoMetaDataFragment";
|
||||
|
||||
private static final String RATING_NONE = "none";
|
||||
private static final String RATING_LIKE = "like";
|
||||
private static final String RATING_DISLIKE = "dislike";
|
||||
|
||||
private Rating videoRating;
|
||||
private ColorStateList defaultTextColor;
|
||||
|
||||
private boolean leaveAppExpected = false;
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_meta, container, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause()
|
||||
{
|
||||
leaveAppExpected = false;
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public boolean isLeaveAppExpected()
|
||||
{
|
||||
return leaveAppExpected;
|
||||
}
|
||||
|
||||
public void updateVideoMeta(Video video, VideoPlayerService mService) {
|
||||
Context context = getContext();
|
||||
Activity activity = getActivity();
|
||||
|
||||
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
|
||||
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
|
||||
|
||||
// Thumbs up
|
||||
Button thumbsUpButton = activity.findViewById(R.id.video_thumbs_up);
|
||||
defaultTextColor = thumbsUpButton.getTextColors();
|
||||
thumbsUpButton.setText(R.string.video_thumbs_up_icon);
|
||||
new Iconics.Builder().on(thumbsUpButton).build();
|
||||
thumbsUpButton.setOnClickListener(v -> {
|
||||
rateVideo(true, video);
|
||||
});
|
||||
|
||||
// Thumbs Down
|
||||
Button thumbsDownButton = activity.findViewById(R.id.video_thumbs_down);
|
||||
thumbsDownButton.setText(R.string.video_thumbs_down_icon);
|
||||
new Iconics.Builder().on(thumbsDownButton).build();
|
||||
thumbsDownButton.setOnClickListener(v -> {
|
||||
rateVideo(false, video);
|
||||
});
|
||||
|
||||
// video rating
|
||||
videoRating = new Rating();
|
||||
videoRating.setRating(RATING_NONE); // default
|
||||
updateVideoRating(video);
|
||||
|
||||
// Retrieve which rating the user gave to this video
|
||||
if (Session.getInstance().isLoggedIn()) {
|
||||
Call<Rating> call = videoDataService.getVideoRating(video.getId());
|
||||
call.enqueue(new Callback<Rating>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(Call<Rating> call, Response<Rating> response) {
|
||||
videoRating = response.body();
|
||||
updateVideoRating(video);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<Rating> call, Throwable t) {
|
||||
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
|
||||
// Do nothing.
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Share
|
||||
Button videoShareButton = activity.findViewById(R.id.video_share);
|
||||
videoShareButton.setText(R.string.video_share_icon);
|
||||
new Iconics.Builder().on(videoShareButton).build();
|
||||
videoShareButton.setOnClickListener(v ->
|
||||
{
|
||||
leaveAppExpected = true;
|
||||
Intents.Share( context, video );
|
||||
} );
|
||||
|
||||
// Download
|
||||
Button videoDownloadButton = activity.findViewById(R.id.video_download);
|
||||
videoDownloadButton.setText(R.string.video_download_icon);
|
||||
new Iconics.Builder().on(videoDownloadButton).build();
|
||||
videoDownloadButton.setOnClickListener(v -> {
|
||||
// get permission to store file
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
|
||||
leaveAppExpected = true;
|
||||
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE}, 0);
|
||||
if (ActivityCompat.checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) == PackageManager.PERMISSION_GRANTED) {
|
||||
Intents.Download(context, video);
|
||||
} else {
|
||||
Toast.makeText(context, getString(R.string.video_download_permission_error), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
} else {
|
||||
Intents.Download(context, video);
|
||||
}
|
||||
});
|
||||
|
||||
Account account = video.getAccount();
|
||||
|
||||
// owner / creator Avatar
|
||||
Avatar avatar = account.getAvatar();
|
||||
if (avatar != null) {
|
||||
ImageView avatarView = activity.findViewById(R.id.avatar);
|
||||
String baseUrl = APIUrlHelper.getUrl(context);
|
||||
String avatarPath = avatar.getPath();
|
||||
Picasso.get()
|
||||
.load(baseUrl + avatarPath)
|
||||
.into(avatarView);
|
||||
}
|
||||
|
||||
|
||||
// title / name
|
||||
TextView videoName = activity.findViewById(R.id.sl_row_name);
|
||||
videoName.setText(video.getName());
|
||||
|
||||
// created at / views
|
||||
TextView videoMeta = activity.findViewById(R.id.videoMeta);
|
||||
videoMeta.setText(
|
||||
MetaDataHelper.getMetaString(
|
||||
video.getCreatedAt(),
|
||||
video.getViews(),
|
||||
context
|
||||
)
|
||||
);
|
||||
|
||||
// owner / creator
|
||||
TextView videoOwner = activity.findViewById(R.id.videoOwner);
|
||||
videoOwner.setText(
|
||||
MetaDataHelper.getOwnerString(video.getAccount().getName(),
|
||||
video.getAccount().getHost(),
|
||||
context
|
||||
)
|
||||
);
|
||||
|
||||
// description
|
||||
TextView videoDescription = activity.findViewById(R.id.description);
|
||||
String shortDescription = video.getDescription();
|
||||
if (shortDescription != null && Objects.requireNonNull(shortDescription).length() > 237) {
|
||||
shortDescription += "\n" + getString(R.string.video_description_read_more);
|
||||
videoDescription.setOnClickListener(v -> {
|
||||
Call<Description> call = videoDataService.getVideoFullDescription(video.getUuid());
|
||||
call.enqueue(new Callback<Description>() {
|
||||
@Override
|
||||
public void onResponse(Call<Description> call, Response<Description> response) {
|
||||
if (response.isSuccessful() && response.body() != null) {
|
||||
new Description();
|
||||
Description videoFullDescription;
|
||||
videoFullDescription = response.body();
|
||||
videoDescription.setText(videoFullDescription.getDescription());
|
||||
}
|
||||
}
|
||||
@Override
|
||||
public void onFailure(Call<Description> call, Throwable t) {
|
||||
Toast.makeText(getContext(), getString(R.string.video_get_full_description_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
videoDescription.setText(shortDescription);
|
||||
|
||||
// video privacy
|
||||
TextView videoPrivacy = activity.findViewById(R.id.video_privacy);
|
||||
videoPrivacy.setText(video.getPrivacy().getLabel());
|
||||
|
||||
// video category
|
||||
TextView videoCategory = activity.findViewById(R.id.video_category);
|
||||
videoCategory.setText(video.getCategory().getLabel());
|
||||
|
||||
// video privacy
|
||||
TextView videoLicense = activity.findViewById(R.id.video_license);
|
||||
videoLicense.setText(video.getLicence().getLabel());
|
||||
|
||||
// video language
|
||||
TextView videoLanguage = activity.findViewById(R.id.video_language);
|
||||
videoLanguage.setText(video.getLanguage().getLabel());
|
||||
|
||||
// video privacy
|
||||
TextView videoTags = activity.findViewById(R.id.video_tags);
|
||||
videoTags.setText(android.text.TextUtils.join(", ", video.getTags()));
|
||||
|
||||
// more button
|
||||
TextView moreButton = activity.findViewById(R.id.moreButton);
|
||||
moreButton.setText(R.string.video_more_icon);
|
||||
new Iconics.Builder().on(moreButton).build();
|
||||
|
||||
moreButton.setOnClickListener(v -> {
|
||||
PopupMenu popup = new PopupMenu(context, v);
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.video_more_report:
|
||||
Log.v(TAG, "Report");
|
||||
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
case R.id.video_more_blacklist:
|
||||
Log.v(TAG, "Blacklist");
|
||||
Toast.makeText(context, "Not Implemented", Toast.LENGTH_SHORT).show();
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
});
|
||||
popup.inflate(R.menu.menu_video_more);
|
||||
popup.show();
|
||||
});
|
||||
|
||||
// video player options
|
||||
TextView videoOptions = activity.findViewById(R.id.exo_more);
|
||||
videoOptions.setText(R.string.video_more_icon);
|
||||
new Iconics.Builder().on(videoOptions).build();
|
||||
|
||||
videoOptions.setOnClickListener(v -> {
|
||||
VideoOptionsFragment videoOptionsFragment =
|
||||
VideoOptionsFragment.newInstance(mService, video.getFiles());
|
||||
videoOptionsFragment.show(getActivity().getSupportFragmentManager(),
|
||||
VideoOptionsFragment.TAG);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
void updateVideoRating(Video video) {
|
||||
Button thumbsUpButton = getActivity().findViewById(R.id.video_thumbs_up);
|
||||
Button thumbsDownButton = getActivity().findViewById(R.id.video_thumbs_down);
|
||||
|
||||
TypedValue typedValue = new TypedValue();
|
||||
|
||||
TypedArray a = getContext().obtainStyledAttributes(typedValue.data, new int[]{R.attr.colorPrimary});
|
||||
int accentColor = a.getColor(0, 0);
|
||||
|
||||
// Change the color of the thumbs
|
||||
switch (videoRating.getRating()) {
|
||||
case RATING_NONE:
|
||||
thumbsUpButton.setTextColor(defaultTextColor);
|
||||
thumbsDownButton.setTextColor(defaultTextColor);
|
||||
break;
|
||||
case RATING_LIKE:
|
||||
thumbsUpButton.setTextColor(accentColor);
|
||||
thumbsDownButton.setTextColor(defaultTextColor);
|
||||
break;
|
||||
case RATING_DISLIKE:
|
||||
thumbsUpButton.setTextColor(defaultTextColor);
|
||||
thumbsDownButton.setTextColor(accentColor);
|
||||
break;
|
||||
}
|
||||
|
||||
// Update the texts
|
||||
TextView thumbsDownTotal = getActivity().findViewById(R.id.video_thumbs_down_total);
|
||||
TextView thumbsUpTotal = getActivity().findViewById(R.id.video_thumbs_up_total);
|
||||
thumbsUpTotal.setText(String.valueOf(video.getLikes()));
|
||||
thumbsDownTotal.setText(String.valueOf(video.getDislikes()));
|
||||
|
||||
a.recycle();
|
||||
}
|
||||
|
||||
void rateVideo(Boolean like, Video video) {
|
||||
if (Session.getInstance().isLoggedIn()) {
|
||||
final String ratePayload;
|
||||
|
||||
switch (videoRating.getRating()) {
|
||||
case RATING_LIKE:
|
||||
ratePayload = like ? RATING_NONE : RATING_DISLIKE;
|
||||
break;
|
||||
case RATING_DISLIKE:
|
||||
ratePayload = like ? RATING_LIKE : RATING_NONE;
|
||||
break;
|
||||
case RATING_NONE:
|
||||
default:
|
||||
ratePayload = like ? RATING_LIKE : RATING_DISLIKE;
|
||||
break;
|
||||
}
|
||||
|
||||
RequestBody body = RequestBody.create(okhttp3.MediaType.parse("application/json"), "{\"rating\":\"" + ratePayload + "\"}");
|
||||
|
||||
String apiBaseURL = APIUrlHelper.getUrlWithVersion(getContext());
|
||||
GetVideoDataService videoDataService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(getContext())).create(GetVideoDataService.class);
|
||||
|
||||
Call<ResponseBody> call = videoDataService.rateVideo(video.getId(), body);
|
||||
|
||||
call.enqueue(new Callback<ResponseBody>() {
|
||||
|
||||
@Override
|
||||
public void onResponse(Call<ResponseBody> call, Response<ResponseBody> response) {
|
||||
//Log.v(TAG, response.toString());
|
||||
|
||||
// if 20x, update likes/dislikes
|
||||
if (response.isSuccessful()) {
|
||||
String previousRating = videoRating.getRating();
|
||||
|
||||
// Update the likes/dislikes count of the video, if needed.
|
||||
// This is only a visual trick, as the actual like/dislike count has
|
||||
// already been modified on the PeerTube instance.
|
||||
if (!previousRating.equals(ratePayload)) {
|
||||
switch (previousRating) {
|
||||
case RATING_NONE:
|
||||
if (ratePayload.equals(RATING_LIKE)) {
|
||||
video.setLikes(video.getLikes() + 1);
|
||||
} else {
|
||||
video.setDislikes(video.getDislikes() + 1);
|
||||
}
|
||||
break;
|
||||
case RATING_LIKE:
|
||||
video.setLikes(video.getLikes() - 1);
|
||||
if (ratePayload.equals(RATING_DISLIKE)) {
|
||||
video.setDislikes(video.getDislikes() + 1);
|
||||
}
|
||||
break;
|
||||
case RATING_DISLIKE:
|
||||
video.setDislikes(video.getDislikes() - 1);
|
||||
if (ratePayload.equals(RATING_LIKE)) {
|
||||
video.setLikes(video.getLikes() + 1);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
videoRating.setRating(ratePayload);
|
||||
updateVideoRating(video);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(Call<ResponseBody> call, Throwable t) {
|
||||
Toast.makeText(getContext(), getString(R.string.video_rating_failed), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
Toast.makeText(getContext(), getString(R.string.video_login_required_for_service), Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,240 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.fragment
|
||||
|
||||
import android.Manifest
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
|
||||
import android.content.res.ColorStateList
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.os.Bundle
|
||||
import net.schueller.peertube.R
|
||||
import net.schueller.peertube.service.VideoPlayerService
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import net.schueller.peertube.helper.ErrorHelper
|
||||
import androidx.core.app.ActivityCompat
|
||||
import android.content.pm.PackageManager
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.squareup.picasso.Picasso
|
||||
import android.widget.TextView
|
||||
import android.util.TypedValue
|
||||
import android.view.View
|
||||
import android.widget.Button
|
||||
import android.widget.ImageView
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentActivity
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import com.mikepenz.iconics.Iconics
|
||||
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
|
||||
import net.schueller.peertube.intents.Intents
|
||||
import net.schueller.peertube.model.CommentThread
|
||||
import net.schueller.peertube.model.Rating
|
||||
import net.schueller.peertube.model.Video
|
||||
import net.schueller.peertube.model.VideoList
|
||||
import net.schueller.peertube.model.ui.VideoMetaViewItem
|
||||
import net.schueller.peertube.network.GetUserService
|
||||
import net.schueller.peertube.network.Session
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.Exception
|
||||
|
||||
class VideoMetaDataFragment : Fragment() {
|
||||
private var videoRating: Rating? = null
|
||||
private var defaultTextColor: ColorStateList? = null
|
||||
private var recyclerView: RecyclerView? = null
|
||||
private var mMultiViewAdapter: MultiViewRecycleViewAdapter? = null
|
||||
|
||||
private lateinit var videoDescriptionFragment: VideoDescriptionFragment
|
||||
|
||||
var isLeaveAppExpected = false
|
||||
private set
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_meta, container, false)
|
||||
}
|
||||
|
||||
override fun onPause() {
|
||||
isLeaveAppExpected = false
|
||||
super.onPause()
|
||||
}
|
||||
|
||||
fun showDescriptionFragment(video: Video) {
|
||||
// show full description fragment
|
||||
videoDescriptionFragment = VideoDescriptionFragment.newInstance(video, this)
|
||||
childFragmentManager.beginTransaction()
|
||||
.add(R.id.video_meta_data_fragment, videoDescriptionFragment, VideoDescriptionFragment.TAG).commit()
|
||||
}
|
||||
|
||||
fun hideDescriptionFragment() {
|
||||
val fragment: Fragment? = childFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
|
||||
if (fragment != null) {
|
||||
childFragmentManager.beginTransaction().remove(fragment).commit()
|
||||
}
|
||||
}
|
||||
|
||||
fun updateVideoMeta(video: Video, mService: VideoPlayerService?) {
|
||||
|
||||
// Remove description if it is open as we are loading a new video
|
||||
hideDescriptionFragment()
|
||||
|
||||
val context = context
|
||||
val activity: Activity? = activity
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val videoDataService = RetrofitInstance.getRetrofitInstance(
|
||||
apiBaseURL,
|
||||
APIUrlHelper.useInsecureConnection(context)
|
||||
).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
|
||||
// related videos
|
||||
recyclerView = activity!!.findViewById(R.id.relatedVideosView)
|
||||
val layoutManager: RecyclerView.LayoutManager = LinearLayoutManager(this@VideoMetaDataFragment.context)
|
||||
recyclerView?.layoutManager = layoutManager
|
||||
mMultiViewAdapter = MultiViewRecycleViewAdapter(this)
|
||||
recyclerView?.adapter = mMultiViewAdapter
|
||||
|
||||
val videoMetaViewItem = VideoMetaViewItem()
|
||||
videoMetaViewItem.video = video
|
||||
mMultiViewAdapter?.setVideoMeta(videoMetaViewItem)
|
||||
|
||||
loadVideos()
|
||||
|
||||
// loadComments(video.id)
|
||||
|
||||
// mMultiViewAdapter?.setVideoComment()
|
||||
|
||||
// videoOwnerSubscribeButton
|
||||
|
||||
|
||||
// description
|
||||
|
||||
|
||||
// video player options
|
||||
val videoOptions = activity.findViewById<TextView>(R.id.exo_more)
|
||||
videoOptions.setText(R.string.video_more_icon)
|
||||
Iconics.Builder().on(videoOptions).build()
|
||||
videoOptions.setOnClickListener {
|
||||
val videoOptionsFragment = VideoOptionsFragment.newInstance(mService, video.files)
|
||||
videoOptionsFragment.show(
|
||||
getActivity()!!.supportFragmentManager,
|
||||
VideoOptionsFragment.TAG
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadComments(videoId: Int) {
|
||||
val context = context
|
||||
|
||||
val start = 0
|
||||
val count = 1
|
||||
val sort = "-createdAt"
|
||||
|
||||
|
||||
// We set this to default to null so that on initial start there are videos listed.
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val service =
|
||||
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val call: Call<CommentThread> = service.getCommentThreads(videoId, start, count, sort)
|
||||
|
||||
call.enqueue(object : Callback<CommentThread?> {
|
||||
override fun onResponse(call: Call<CommentThread?>, response: Response<CommentThread?>) {
|
||||
if (response.body() != null) {
|
||||
val commentThread = response.body()
|
||||
if (commentThread != null) {
|
||||
mMultiViewAdapter!!.setVideoComment(commentThread);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<CommentThread?>, t: Throwable) {
|
||||
Log.wtf("err", t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun loadVideos() {
|
||||
val context = context
|
||||
|
||||
val start = 0
|
||||
val count = 6
|
||||
val sort = "-createdAt"
|
||||
val filter: String? = null
|
||||
|
||||
val sharedPref = context?.getSharedPreferences(
|
||||
context.packageName + "_preferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
var nsfw = "false"
|
||||
var languages: Set<String>? = emptySet()
|
||||
if (sharedPref != null) {
|
||||
nsfw = if (sharedPref.getBoolean(getString(R.string.pref_show_nsfw_key), false)) "both" else "false"
|
||||
languages = sharedPref.getStringSet(getString(R.string.pref_video_language_key), null)
|
||||
}
|
||||
|
||||
// We set this to default to null so that on initial start there are videos listed.
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val service =
|
||||
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val call: Call<VideoList> = service.getVideosData(start, count, sort, nsfw, filter, languages)
|
||||
|
||||
/*Log the URL called*/Log.d("URL Called", call.request().url.toString() + "")
|
||||
// Toast.makeText(VideoListActivity.this, "URL Called: " + call.request().url(), Toast.LENGTH_SHORT).show();
|
||||
call.enqueue(object : Callback<VideoList?> {
|
||||
override fun onResponse(call: Call<VideoList?>, response: Response<VideoList?>) {
|
||||
if (response.body() != null) {
|
||||
val videoList = response.body()
|
||||
if (videoList != null) {
|
||||
mMultiViewAdapter!!.setVideoListData(videoList)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<VideoList?>, t: Throwable) {
|
||||
Log.wtf("err", t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(this@VideoMetaDataFragment.context, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
companion object {
|
||||
const val TAG = "VMDF"
|
||||
}
|
||||
}
|
@ -1,516 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.fragment;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.util.Log;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
|
||||
import com.github.se_bastiaan.torrentstream.StreamStatus;
|
||||
import com.github.se_bastiaan.torrentstream.Torrent;
|
||||
import com.github.se_bastiaan.torrentstream.TorrentOptions;
|
||||
import com.github.se_bastiaan.torrentstream.TorrentStream;
|
||||
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener;
|
||||
import com.google.android.exoplayer2.Format;
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters;
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
|
||||
import com.google.android.exoplayer2.ui.PlayerView;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener;
|
||||
import com.mikepenz.iconics.Iconics;
|
||||
|
||||
import net.schueller.peertube.R;
|
||||
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.helper.ErrorHelper;
|
||||
import net.schueller.peertube.model.File;
|
||||
import net.schueller.peertube.model.Video;
|
||||
import net.schueller.peertube.network.GetVideoDataService;
|
||||
import net.schueller.peertube.network.RetrofitInstance;
|
||||
import net.schueller.peertube.service.VideoPlayerService;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.fragment.app.Fragment;
|
||||
|
||||
import retrofit2.Call;
|
||||
import retrofit2.Callback;
|
||||
import retrofit2.Response;
|
||||
|
||||
import static net.schueller.peertube.helper.VideoHelper.canEnterPipMode;
|
||||
|
||||
public class VideoPlayerFragment extends Fragment implements VideoRendererEventListener {
|
||||
|
||||
private String mVideoUuid;
|
||||
private ProgressBar progressBar;
|
||||
private PlayerView simpleExoPlayerView;
|
||||
private Intent videoPlayerIntent;
|
||||
private Boolean mBound = false;
|
||||
private Boolean isFullscreen = false;
|
||||
private VideoPlayerService mService;
|
||||
private TorrentStream torrentStream;
|
||||
private LinearLayout torrentStatus;
|
||||
private float aspectRatio;
|
||||
|
||||
private static final String TAG = "VideoPlayerFragment";
|
||||
private GestureDetector mDetector;
|
||||
|
||||
private ServiceConnection mConnection = new ServiceConnection() {
|
||||
|
||||
@Override
|
||||
public void onServiceConnected(ComponentName className, IBinder service) {
|
||||
Log.d(TAG, "onServiceConnected");
|
||||
VideoPlayerService.LocalBinder binder = (VideoPlayerService.LocalBinder) service;
|
||||
mService = binder.getService();
|
||||
|
||||
// 2. Create the player
|
||||
simpleExoPlayerView.setPlayer(mService.player);
|
||||
mBound = true;
|
||||
|
||||
loadVideo();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onServiceDisconnected(ComponentName componentName) {
|
||||
Log.d(TAG, "onServiceDisconnected");
|
||||
simpleExoPlayerView.setPlayer(null);
|
||||
mBound = false;
|
||||
}
|
||||
};
|
||||
private AspectRatioFrameLayout.AspectRatioListener aspectRatioListerner = new AspectRatioFrameLayout.AspectRatioListener()
|
||||
{
|
||||
@Override
|
||||
public void onAspectRatioUpdated( float targetAspectRatio, float naturalAspectRatio, boolean aspectRatioMismatch )
|
||||
{
|
||||
aspectRatio = targetAspectRatio;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(R.layout.fragment_video_player, container, false);
|
||||
}
|
||||
|
||||
|
||||
public void start(String videoUuid) {
|
||||
|
||||
// start service
|
||||
Context context = getContext();
|
||||
Activity activity = getActivity();
|
||||
|
||||
mVideoUuid = videoUuid;
|
||||
|
||||
assert activity != null;
|
||||
progressBar = activity.findViewById(R.id.torrent_progress);
|
||||
progressBar.setMax(100);
|
||||
|
||||
assert context != null;
|
||||
simpleExoPlayerView = new PlayerView(context);
|
||||
simpleExoPlayerView = activity.findViewById(R.id.video_view);
|
||||
|
||||
simpleExoPlayerView.setControllerShowTimeoutMs(1000);
|
||||
simpleExoPlayerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
|
||||
|
||||
mDetector = new GestureDetector(context, new MyGestureListener());
|
||||
simpleExoPlayerView.setOnTouchListener(touchListener);
|
||||
|
||||
simpleExoPlayerView.setAspectRatioListener( aspectRatioListerner );
|
||||
|
||||
torrentStatus = activity.findViewById(R.id.exo_torrent_status);
|
||||
|
||||
// Full screen Icon
|
||||
TextView fullscreenText = activity.findViewById(R.id.exo_fullscreen);
|
||||
FrameLayout fullscreenButton = activity.findViewById(R.id.exo_fullscreen_button);
|
||||
|
||||
fullscreenText.setText(R.string.video_expand_icon);
|
||||
new Iconics.Builder().on(fullscreenText).build();
|
||||
|
||||
fullscreenButton.setOnClickListener(view -> {
|
||||
Log.d(TAG, "Fullscreen");
|
||||
fullScreenToggle();
|
||||
});
|
||||
|
||||
if (!mBound) {
|
||||
videoPlayerIntent = new Intent(context, VideoPlayerService.class);
|
||||
activity.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void loadVideo() {
|
||||
Context context = getContext();
|
||||
|
||||
|
||||
// get video details from api
|
||||
String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
|
||||
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(GetVideoDataService.class);
|
||||
|
||||
Call<Video> call = service.getVideoData(mVideoUuid);
|
||||
|
||||
call.enqueue(new Callback<Video>() {
|
||||
@Override
|
||||
public void onResponse(@NonNull Call<Video> call, @NonNull Response<Video> response) {
|
||||
|
||||
Video video = response.body();
|
||||
|
||||
mService.setCurrentVideo(video);
|
||||
|
||||
if (video == null) {
|
||||
Toast.makeText(context, "Unable to retrieve video information, try again later.", Toast.LENGTH_SHORT).show();
|
||||
return;
|
||||
}
|
||||
|
||||
playVideo(video);
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFailure(@NonNull Call<Video> call, @NonNull Throwable t) {
|
||||
Log.wtf(TAG, t.fillInStackTrace());
|
||||
ErrorHelper.showToastFromCommunicationError( getActivity(), t );
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void useController(boolean value) {
|
||||
if (mBound) {
|
||||
simpleExoPlayerView.setUseController(value);
|
||||
}
|
||||
}
|
||||
|
||||
private void playVideo(Video video) {
|
||||
|
||||
Context context = getContext();
|
||||
|
||||
// video Meta fragment
|
||||
VideoMetaDataFragment videoMetaDataFragment = (VideoMetaDataFragment)
|
||||
requireActivity().getSupportFragmentManager().findFragmentById(R.id.video_meta_data_fragment);
|
||||
|
||||
assert videoMetaDataFragment != null;
|
||||
videoMetaDataFragment.updateVideoMeta(video, mService);
|
||||
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
|
||||
if (sharedPref.getBoolean(getString(R.string.pref_torrent_player_key), false)) {
|
||||
torrentStatus.setVisibility(View.VISIBLE);
|
||||
String stream = video.getFiles().get(0).getTorrentUrl();
|
||||
Log.v(TAG, "getTorrentUrl : " + video.getFiles().get(0).getTorrentUrl());
|
||||
torrentStream = setupTorrentStream();
|
||||
torrentStream.startStream(stream);
|
||||
} else {
|
||||
|
||||
Integer videoQuality = sharedPref.getInt(getString(R.string.pref_quality_key), 999999);
|
||||
|
||||
String urlToPlay = null;
|
||||
boolean isHLS = false;
|
||||
|
||||
// try HLS stream first
|
||||
// get video qualities
|
||||
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
|
||||
if (video.getStreamingPlaylists().size() > 0) {
|
||||
urlToPlay = video.getStreamingPlaylists().get( 0 ).getPlaylistUrl();
|
||||
isHLS = true;
|
||||
} else {
|
||||
if (video.getFiles().size() > 0) {
|
||||
urlToPlay = video.getFiles().get( 0 ).getFileUrl(); // default, take first found, usually highest res
|
||||
for ( File file : video.getFiles() ) {
|
||||
// Set quality if it matches
|
||||
if ( file.getResolution().getId().equals( videoQuality ) ) {
|
||||
urlToPlay = file.getFileUrl();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!urlToPlay.isEmpty()) {
|
||||
mService.setCurrentStreamUrl( urlToPlay, isHLS);
|
||||
torrentStatus.setVisibility(View.GONE);
|
||||
startPlayer();
|
||||
} else {
|
||||
stopVideo();
|
||||
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
Log.v(TAG, "end of load Video");
|
||||
|
||||
|
||||
}
|
||||
|
||||
private void startPlayer() {
|
||||
Util.startForegroundService(requireContext(), videoPlayerIntent);
|
||||
}
|
||||
|
||||
|
||||
public void destroyVideo() {
|
||||
simpleExoPlayerView.setPlayer(null);
|
||||
if (torrentStream != null) {
|
||||
torrentStream.stopStream();
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseVideo() {
|
||||
if (mBound) {
|
||||
mService.player.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
|
||||
public void pauseToggle() {
|
||||
if (mBound) {
|
||||
mService.player.setPlayWhenReady(!mService.player.getPlayWhenReady());
|
||||
}
|
||||
}
|
||||
|
||||
public void unPauseVideo() {
|
||||
if (mBound) {
|
||||
mService.player.setPlayWhenReady(true);
|
||||
}
|
||||
}
|
||||
|
||||
public float getVideoAspectRatio() { return aspectRatio; }
|
||||
|
||||
public boolean isPaused() {
|
||||
return !mService.player.getPlayWhenReady();
|
||||
}
|
||||
|
||||
public void showControls(boolean value) {
|
||||
simpleExoPlayerView.setUseController(value);
|
||||
}
|
||||
|
||||
public void stopVideo() {
|
||||
|
||||
if (mBound) {
|
||||
requireContext().unbindService(mConnection);
|
||||
mBound = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void setIsFullscreen(Boolean fullscreen) {
|
||||
isFullscreen = fullscreen;
|
||||
|
||||
TextView fullscreenButton = requireActivity().findViewById(R.id.exo_fullscreen);
|
||||
if (fullscreen) {
|
||||
fullscreenButton.setText(R.string.video_compress_icon);
|
||||
} else {
|
||||
fullscreenButton.setText(R.string.video_expand_icon);
|
||||
}
|
||||
new Iconics.Builder().on(fullscreenButton).build();
|
||||
}
|
||||
|
||||
public Boolean getIsFullscreen() {
|
||||
return isFullscreen;
|
||||
}
|
||||
|
||||
public void fullScreenToggle() {
|
||||
if (!isFullscreen) {
|
||||
setIsFullscreen(true);
|
||||
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else {
|
||||
setIsFullscreen(false);
|
||||
requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Torrent Playback
|
||||
*
|
||||
* @return torrent stream
|
||||
*/
|
||||
private TorrentStream setupTorrentStream() {
|
||||
|
||||
TorrentOptions torrentOptions = new TorrentOptions.Builder()
|
||||
.saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
|
||||
.removeFilesAfterStop(true)
|
||||
.build();
|
||||
|
||||
TorrentStream torrentStream = TorrentStream.init(torrentOptions);
|
||||
|
||||
torrentStream.addListener(new TorrentListener() {
|
||||
@Override
|
||||
public void onStreamReady(Torrent torrent) {
|
||||
String videopath = Uri.fromFile(torrent.getVideoFile()).toString();
|
||||
Log.d(TAG, "Ready! torrentStream videopath:" + videopath);
|
||||
mService.setCurrentStreamUrl(videopath, false);
|
||||
startPlayer();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamProgress(Torrent torrent, StreamStatus streamStatus) {
|
||||
if (streamStatus.bufferProgress <= 100 && progressBar.getProgress() < 100 && progressBar.getProgress() != streamStatus.bufferProgress) {
|
||||
//Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
|
||||
progressBar.setProgress(streamStatus.bufferProgress);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamStopped() {
|
||||
Log.d(TAG, "Stopped");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamPrepared(Torrent torrent) {
|
||||
Log.d(TAG, "Prepared");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamStarted(Torrent torrent) {
|
||||
Log.d(TAG, "Started");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStreamError(Torrent torrent, Exception e) {
|
||||
Log.d(TAG, "Error: " + e.getMessage());
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
return torrentStream;
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onVideoEnabled(DecoderCounters counters) {
|
||||
Log.v(TAG, "onVideoEnabled()...");
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoDecoderInitialized(String decoderName, long initializedTimestampMs, long initializationDurationMs) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoInputFormatChanged(Format format) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDroppedFrames(int count, long elapsedMs) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoSizeChanged(int width, int height, int unappliedRotationDegrees, float pixelWidthHeightRatio) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRenderedFirstFrame(Surface surface) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVideoDisabled(DecoderCounters counters) {
|
||||
Log.v(TAG, "onVideoDisabled()...");
|
||||
}
|
||||
|
||||
View.OnTouchListener touchListener = new View.OnTouchListener() {
|
||||
@Override
|
||||
public boolean onTouch(View v, MotionEvent event) {
|
||||
return mDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
public String getVideoUuid() {
|
||||
return mVideoUuid;
|
||||
}
|
||||
|
||||
class MyGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
/*
|
||||
@Override
|
||||
public boolean onDown(MotionEvent event) {
|
||||
Log.d("TAG","onDown: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
Log.i("TAG", "onSingleTapConfirmed: ");
|
||||
pauseToggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
Log.i("TAG", "onLongPress: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
Log.i("TAG", "onDoubleTap: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2,
|
||||
float distanceX, float distanceY) {
|
||||
Log.i("TAG", "onScroll: ");
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.N)
|
||||
@Override
|
||||
public boolean onFling(MotionEvent event1, MotionEvent event2,
|
||||
float velocityX, float velocityY) {
|
||||
Log.d(TAG, event1.toString());
|
||||
Log.d(TAG, event2.toString());
|
||||
Log.d(TAG, String.valueOf(velocityX));
|
||||
Log.d(TAG, String.valueOf(velocityY));
|
||||
//arbitrarily velocity speeds that seem to work to differentiate events.
|
||||
if (velocityY > 4000) {
|
||||
Log.d(TAG, "we have a drag down event");
|
||||
if (canEnterPipMode(getContext())) {
|
||||
requireActivity().enterPictureInPictureMode();
|
||||
}
|
||||
}
|
||||
if ((velocityX > 2000) && (Math.abs(velocityY) < 2000)) {
|
||||
Log.d(TAG, "swipe right " + velocityY);
|
||||
}
|
||||
if ((velocityX < 2000) && (Math.abs(velocityY) < 2000)) {
|
||||
Log.d(TAG, "swipe left " + velocityY);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,496 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.fragment
|
||||
|
||||
import com.google.android.exoplayer2.video.VideoRendererEventListener
|
||||
import com.google.android.exoplayer2.ui.PlayerView
|
||||
import android.content.Intent
|
||||
import net.schueller.peertube.service.VideoPlayerService
|
||||
import com.github.se_bastiaan.torrentstream.TorrentStream
|
||||
import android.widget.LinearLayout
|
||||
import android.view.GestureDetector
|
||||
import android.content.ServiceConnection
|
||||
import android.content.ComponentName
|
||||
import android.os.IBinder
|
||||
import net.schueller.peertube.service.VideoPlayerService.LocalBinder
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.AspectRatioListener
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import android.os.Bundle
|
||||
import net.schueller.peertube.R
|
||||
import android.app.Activity
|
||||
import android.app.PictureInPictureParams
|
||||
import android.content.Context
|
||||
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
|
||||
import android.widget.TextView
|
||||
import android.widget.FrameLayout
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.network.GetVideoDataService
|
||||
import net.schueller.peertube.network.RetrofitInstance
|
||||
import android.widget.Toast
|
||||
import net.schueller.peertube.helper.ErrorHelper
|
||||
import android.content.pm.ActivityInfo
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener
|
||||
import com.github.se_bastiaan.torrentstream.Torrent
|
||||
import com.github.se_bastiaan.torrentstream.StreamStatus
|
||||
import com.google.android.exoplayer2.decoder.DecoderCounters
|
||||
import android.view.View.OnTouchListener
|
||||
import android.view.MotionEvent
|
||||
import android.view.GestureDetector.SimpleOnGestureListener
|
||||
import androidx.annotation.RequiresApi
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Environment
|
||||
import android.util.Log
|
||||
import android.view.View
|
||||
import android.widget.ProgressBar
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import androidx.fragment.app.Fragment
|
||||
import com.github.se_bastiaan.torrentstream.TorrentOptions.Builder
|
||||
import com.google.android.exoplayer2.Format
|
||||
import com.google.android.exoplayer2.util.Util
|
||||
import com.mikepenz.iconics.Iconics
|
||||
import net.schueller.peertube.R.layout
|
||||
import net.schueller.peertube.R.string
|
||||
import net.schueller.peertube.helper.VideoHelper
|
||||
import net.schueller.peertube.model.Video
|
||||
import retrofit2.Call
|
||||
import retrofit2.Callback
|
||||
import retrofit2.Response
|
||||
import java.lang.Exception
|
||||
import kotlin.math.abs
|
||||
|
||||
class VideoPlayerFragment : Fragment(), VideoRendererEventListener {
|
||||
|
||||
var videoUuid: String? = null
|
||||
private set
|
||||
// private var progressBar: ProgressBar? = null
|
||||
private var exoPlayer: PlayerView? = null
|
||||
private var videoPlayerIntent: Intent? = null
|
||||
private var mBound = false
|
||||
private var isFullscreen = false
|
||||
private var mService: VideoPlayerService? = null
|
||||
private var torrentStream: TorrentStream? = null
|
||||
// private var torrentStatus: LinearLayout? = null
|
||||
var videoAspectRatio = 0f
|
||||
private set
|
||||
private var mDetector: GestureDetector? = null
|
||||
private val mConnection: ServiceConnection = object : ServiceConnection {
|
||||
override fun onServiceConnected(className: ComponentName, service: IBinder) {
|
||||
Log.d(TAG, "onServiceConnected")
|
||||
val binder = service as LocalBinder
|
||||
mService = binder.service
|
||||
|
||||
// 2. Create the player
|
||||
exoPlayer!!.player = mService!!.player
|
||||
mBound = true
|
||||
loadVideo()
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(componentName: ComponentName) {
|
||||
Log.d(TAG, "onServiceDisconnected")
|
||||
exoPlayer!!.player = null
|
||||
mBound = false
|
||||
}
|
||||
}
|
||||
private val aspectRatioListener: AspectRatioListener = AspectRatioListener {
|
||||
targetAspectRatio, _, _ -> videoAspectRatio = targetAspectRatio
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
inflater: LayoutInflater, container: ViewGroup?,
|
||||
savedInstanceState: Bundle?
|
||||
): View? {
|
||||
|
||||
// Inflate the layout for this fragment
|
||||
return inflater.inflate(layout.fragment_video_player, container, false)
|
||||
}
|
||||
|
||||
fun start(videoUuid: String?) {
|
||||
|
||||
// start service
|
||||
val context = context
|
||||
val activity: Activity? = activity
|
||||
this.videoUuid = videoUuid
|
||||
assert(activity != null)
|
||||
// progressBar = activity?.findViewById(R.id.torrent_progress)
|
||||
// progressBar?.max = 100
|
||||
assert(context != null)
|
||||
exoPlayer = PlayerView(context!!)
|
||||
exoPlayer = activity?.findViewById(R.id.video_view)
|
||||
exoPlayer?.controllerShowTimeoutMs = 1000
|
||||
exoPlayer?.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
|
||||
mDetector = GestureDetector(context, MyGestureListener())
|
||||
exoPlayer?.setOnTouchListener(touchListener)
|
||||
exoPlayer?.setAspectRatioListener(aspectRatioListener)
|
||||
// torrentStatus = activity?.findViewById(R.id.exo_torrent_status)
|
||||
|
||||
// Full screen Icon
|
||||
val fullscreenText = activity?.findViewById<TextView>(R.id.exo_fullscreen)
|
||||
val fullscreenButton = activity?.findViewById<FrameLayout>(R.id.exo_fullscreen_button)
|
||||
fullscreenText?.setText(string.video_expand_icon)
|
||||
if (fullscreenText != null) {
|
||||
Iconics.Builder().on(fullscreenText).build()
|
||||
fullscreenButton?.setOnClickListener {
|
||||
Log.d(TAG, "Fullscreen")
|
||||
fullScreenToggle()
|
||||
}
|
||||
}
|
||||
if (!mBound) {
|
||||
videoPlayerIntent = Intent(context, VideoPlayerService::class.java)
|
||||
activity?.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadVideo() {
|
||||
val context = context
|
||||
|
||||
// get video details from api
|
||||
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
|
||||
val service =
|
||||
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
|
||||
GetVideoDataService::class.java
|
||||
)
|
||||
val call = service.getVideoData(videoUuid)
|
||||
call.enqueue(object : Callback<Video?> {
|
||||
override fun onResponse(call: Call<Video?>, response: Response<Video?>) {
|
||||
val video = response.body()
|
||||
mService!!.setCurrentVideo(video)
|
||||
if (video == null) {
|
||||
Toast.makeText(
|
||||
context,
|
||||
"Unable to retrieve video information, try again later.",
|
||||
Toast.LENGTH_SHORT
|
||||
).show()
|
||||
return
|
||||
}
|
||||
playVideo(video)
|
||||
}
|
||||
|
||||
override fun onFailure(call: Call<Video?>, t: Throwable) {
|
||||
Log.wtf(TAG, t.fillInStackTrace())
|
||||
ErrorHelper.showToastFromCommunicationError(activity, t)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fun useController(value: Boolean) {
|
||||
if (mBound) {
|
||||
exoPlayer!!.useController = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun playVideo(video: Video) {
|
||||
val context = context
|
||||
|
||||
// video Meta fragment
|
||||
val videoMetaDataFragment =
|
||||
(requireActivity().supportFragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?)!!
|
||||
videoMetaDataFragment.updateVideoMeta(video, mService)
|
||||
|
||||
val sharedPref = context?.getSharedPreferences(
|
||||
context.packageName + "_preferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
var prefTorrentPlayer = false
|
||||
var videoQuality = 999999
|
||||
if (sharedPref != null) {
|
||||
prefTorrentPlayer = sharedPref.getBoolean(getString(string.pref_torrent_player_key), false)
|
||||
videoQuality = sharedPref.getInt(getString(string.pref_quality_key), 999999)
|
||||
}
|
||||
|
||||
// if (prefTorrentPlayer) {
|
||||
// torrentStatus!!.visibility = View.VISIBLE
|
||||
// val stream = video.files[0].torrentUrl
|
||||
// Log.v(TAG, "getTorrentUrl : " + video.files[0].torrentUrl)
|
||||
// torrentStream = setupTorrentStream()
|
||||
// torrentStream!!.startStream(stream)
|
||||
// } else {
|
||||
var urlToPlay: String? = null
|
||||
var isHLS = false
|
||||
|
||||
// try HLS stream first
|
||||
// get video qualities
|
||||
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
|
||||
if (video.streamingPlaylists.size > 0) {
|
||||
urlToPlay = video.streamingPlaylists[0].playlistUrl
|
||||
isHLS = true
|
||||
} else {
|
||||
if (video.files.size > 0) {
|
||||
urlToPlay = video.files[0].fileUrl // default, take first found, usually highest res
|
||||
for (file in video.files) {
|
||||
// Set quality if it matches
|
||||
if (file.resolution.id == videoQuality) {
|
||||
urlToPlay = file.fileUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (urlToPlay!!.isNotEmpty()) {
|
||||
mService!!.setCurrentStreamUrl(urlToPlay, isHLS)
|
||||
// torrentStatus!!.visibility = View.GONE
|
||||
startPlayer()
|
||||
} else {
|
||||
stopVideo()
|
||||
Toast.makeText(context, string.api_error, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
// }
|
||||
Log.v(TAG, "end of load Video")
|
||||
}
|
||||
|
||||
private fun startPlayer() {
|
||||
Util.startForegroundService(requireContext(), videoPlayerIntent!!)
|
||||
}
|
||||
|
||||
fun destroyVideo() {
|
||||
exoPlayer!!.player = null
|
||||
if (torrentStream != null) {
|
||||
torrentStream!!.stopStream()
|
||||
}
|
||||
}
|
||||
|
||||
fun pauseVideo() {
|
||||
if (mBound) {
|
||||
mService!!.player!!.playWhenReady = false
|
||||
}
|
||||
}
|
||||
|
||||
// fun pauseToggle() {
|
||||
// if (mBound) {
|
||||
// mService!!.player!!.playWhenReady = !mService!!.player!!.playWhenReady
|
||||
// }
|
||||
// }
|
||||
|
||||
fun unPauseVideo() {
|
||||
if (mBound) {
|
||||
mService!!.player!!.playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
val isPaused: Boolean
|
||||
get() = !mService!!.player!!.playWhenReady
|
||||
|
||||
fun showControls(value: Boolean) {
|
||||
exoPlayer!!.useController = value
|
||||
}
|
||||
|
||||
fun stopVideo() {
|
||||
if (mBound) {
|
||||
requireContext().unbindService(mConnection)
|
||||
mBound = false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* triggered rotation and button press
|
||||
*/
|
||||
fun setIsFullscreen(fullscreen: Boolean) {
|
||||
isFullscreen = fullscreen
|
||||
val fullscreenButton = requireActivity().findViewById<TextView>(R.id.exo_fullscreen)
|
||||
if (fullscreen) {
|
||||
hideSystemBars()
|
||||
fullscreenButton.setText(string.video_compress_icon)
|
||||
} else {
|
||||
restoreSystemBars()
|
||||
fullscreenButton.setText(string.video_expand_icon)
|
||||
}
|
||||
Iconics.Builder().on(fullscreenButton).build()
|
||||
}
|
||||
|
||||
private fun hideSystemBars()
|
||||
{
|
||||
val view = this.view
|
||||
if (view != null) {
|
||||
val windowInsetsController =
|
||||
ViewCompat.getWindowInsetsController(view) ?: return
|
||||
// Configure the behavior of the hidden system bars
|
||||
windowInsetsController.systemBarsBehavior =
|
||||
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
// Hide both the status bar and the navigation bar
|
||||
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
private fun restoreSystemBars()
|
||||
{
|
||||
val view = this.view
|
||||
if (view != null) {
|
||||
val windowInsetsController =
|
||||
ViewCompat.getWindowInsetsController(view) ?: return
|
||||
// Show both the status bar and the navigation bar
|
||||
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
|
||||
}
|
||||
}
|
||||
|
||||
fun getIsFullscreen(): Boolean {
|
||||
return isFullscreen
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered by button press
|
||||
*/
|
||||
fun fullScreenToggle() {
|
||||
if (!isFullscreen) {
|
||||
setIsFullscreen(true)
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
|
||||
} else {
|
||||
setIsFullscreen(false)
|
||||
// we want to force portrait if fullscreen is switched of as we do not have a min. landscape view
|
||||
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
||||
}
|
||||
}
|
||||
//
|
||||
// /**
|
||||
// * Torrent Playback
|
||||
// *
|
||||
// * @return torrent stream
|
||||
// */
|
||||
// private fun setupTorrentStream(): TorrentStream {
|
||||
// val torrentOptions = Builder()
|
||||
// .saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
|
||||
// .removeFilesAfterStop(true)
|
||||
// .build()
|
||||
// val torrentStream = TorrentStream.init(torrentOptions)
|
||||
// torrentStream.addListener(object : TorrentListener {
|
||||
// override fun onStreamReady(torrent: Torrent) {
|
||||
// val videoPath = Uri.fromFile(torrent.videoFile).toString()
|
||||
// Log.d(TAG, "Ready! torrentStream videoPath:$videoPath")
|
||||
// mService!!.setCurrentStreamUrl(videoPath, false)
|
||||
// startPlayer()
|
||||
// }
|
||||
//
|
||||
// override fun onStreamProgress(torrent: Torrent, streamStatus: StreamStatus) {
|
||||
// if (streamStatus.bufferProgress <= 100 && progressBar!!.progress < 100 && progressBar!!.progress != streamStatus.bufferProgress) {
|
||||
// //Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
|
||||
// progressBar!!.progress = streamStatus.bufferProgress
|
||||
// }
|
||||
// }
|
||||
//
|
||||
// override fun onStreamStopped() {
|
||||
// Log.d(TAG, "Stopped")
|
||||
// }
|
||||
//
|
||||
// override fun onStreamPrepared(torrent: Torrent) {
|
||||
// Log.d(TAG, "Prepared")
|
||||
// }
|
||||
//
|
||||
// override fun onStreamStarted(torrent: Torrent) {
|
||||
// Log.d(TAG, "Started")
|
||||
// }
|
||||
//
|
||||
// override fun onStreamError(torrent: Torrent, e: Exception) {
|
||||
// Log.d(TAG, "Error: " + e.message)
|
||||
// }
|
||||
// })
|
||||
// return torrentStream
|
||||
// }
|
||||
|
||||
override fun onVideoEnabled(counters: DecoderCounters) {
|
||||
Log.v(TAG, "onVideoEnabled()...")
|
||||
}
|
||||
|
||||
override fun onVideoDecoderInitialized(
|
||||
decoderName: String,
|
||||
initializedTimestampMs: Long,
|
||||
initializationDurationMs: Long
|
||||
) {
|
||||
}
|
||||
|
||||
override fun onVideoInputFormatChanged(format: Format) {}
|
||||
override fun onDroppedFrames(count: Int, elapsedMs: Long) {}
|
||||
override fun onVideoDisabled(counters: DecoderCounters) {
|
||||
Log.v(TAG, "onVideoDisabled()...")
|
||||
}
|
||||
|
||||
// touch event on video player
|
||||
private var touchListener = OnTouchListener { _, event ->
|
||||
//v.performClick() // causes flicker but should be implemented for accessibility
|
||||
mDetector!!.onTouchEvent(event)
|
||||
}
|
||||
|
||||
internal inner class MyGestureListener : SimpleOnGestureListener() {
|
||||
|
||||
/*
|
||||
@Override
|
||||
public boolean onDown(MotionEvent event) {
|
||||
Log.d("TAG","onDown: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
Log.i("TAG", "onSingleTapConfirmed: ");
|
||||
pauseToggle();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLongPress(MotionEvent e) {
|
||||
Log.i("TAG", "onLongPress: ");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onDoubleTap(MotionEvent e) {
|
||||
Log.i("TAG", "onDoubleTap: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onScroll(MotionEvent e1, MotionEvent e2,
|
||||
float distanceX, float distanceY) {
|
||||
Log.i("TAG", "onScroll: ");
|
||||
return true;
|
||||
}
|
||||
*/
|
||||
@RequiresApi(api = VERSION_CODES.N)
|
||||
override fun onFling(
|
||||
event1: MotionEvent, event2: MotionEvent,
|
||||
velocityX: Float, velocityY: Float
|
||||
): Boolean {
|
||||
Log.d(TAG, event1.toString())
|
||||
Log.d(TAG, event2.toString())
|
||||
Log.d(TAG, velocityX.toString())
|
||||
Log.d(TAG, velocityY.toString())
|
||||
//arbitrarily velocity speeds that seem to work to differentiate events.
|
||||
if (velocityY > 4000) {
|
||||
Log.d(TAG, "we have a drag down event")
|
||||
if (VideoHelper.canEnterPipMode(context)) {
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
|
||||
val pipParams = PictureInPictureParams.Builder()
|
||||
requireActivity().enterPictureInPictureMode(pipParams.build())
|
||||
}
|
||||
}
|
||||
}
|
||||
if (velocityX > 2000 && abs(velocityY) < 2000) {
|
||||
Log.d(TAG, "swipe right $velocityY")
|
||||
}
|
||||
if (velocityX < 2000 && abs(velocityY) < 2000) {
|
||||
Log.d(TAG, "swipe left $velocityY")
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "VideoPlayerFragment"
|
||||
}
|
||||
}
|
@ -1,93 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.intents;
|
||||
|
||||
import android.Manifest;
|
||||
import android.app.DownloadManager;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Environment;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.webkit.URLUtil;
|
||||
import android.widget.Toast;
|
||||
|
||||
|
||||
import com.github.se_bastiaan.torrentstream.TorrentOptions;
|
||||
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.model.Video;
|
||||
|
||||
import androidx.core.app.ActivityCompat;
|
||||
|
||||
|
||||
public class Intents {
|
||||
|
||||
private static final String TAG = "Intents";
|
||||
|
||||
/**
|
||||
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
|
||||
*
|
||||
* @param context context
|
||||
* @param video video
|
||||
*/
|
||||
// TODO, offer which version to download
|
||||
public static void Share(Context context, Video video) {
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setAction(Intent.ACTION_SEND);
|
||||
intent.putExtra(Intent.EXTRA_SUBJECT, video.getName());
|
||||
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.getUuid()) );
|
||||
intent.setType("text/plain");
|
||||
|
||||
context.startActivity(intent);
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param context context
|
||||
* @param video video
|
||||
*/
|
||||
// TODO, offer which version to download
|
||||
public static void Download(Context context, Video video) {
|
||||
|
||||
if (video.getFiles().size() > 0)
|
||||
{
|
||||
String url = video.getFiles().get( 0 ).getFileDownloadUrl();
|
||||
// make sure it is a valid filename
|
||||
String destFilename = video.getName().replaceAll( "[^a-zA-Z0-9]", "_" ) + "." + MimeTypeMap.getFileExtensionFromUrl( URLUtil.guessFileName( url, null, null ) );
|
||||
|
||||
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
|
||||
DownloadManager.Request request = new DownloadManager.Request( Uri.parse( url ) );
|
||||
request.setDescription( video.getDescription() );
|
||||
request.setTitle( video.getName() );
|
||||
request.allowScanningByMediaScanner();
|
||||
request.setNotificationVisibility( DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED );
|
||||
request.setDestinationInExternalPublicDir( Environment.DIRECTORY_DOWNLOADS, destFilename );
|
||||
|
||||
// get download service and enqueue file
|
||||
DownloadManager manager = (DownloadManager) context.getSystemService( Context.DOWNLOAD_SERVICE );
|
||||
manager.enqueue( request );
|
||||
} else {
|
||||
Toast.makeText( context, R.string.api_error, Toast.LENGTH_LONG ).show();
|
||||
}
|
||||
}
|
||||
}
|
146
app/src/main/java/net/schueller/peertube/intents/Intents.kt
Normal file
146
app/src/main/java/net/schueller/peertube/intents/Intents.kt
Normal 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
|
||||
}
|
||||
}
|
38
app/src/main/java/net/schueller/peertube/model/Comment.kt
Normal file
38
app/src/main/java/net/schueller/peertube/model/Comment.kt
Normal 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
|
||||
|
||||
)
|
@ -0,0 +1,29 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
|
||||
import java.util.ArrayList
|
||||
|
||||
class CommentThread(
|
||||
val total: Int,
|
||||
val totalNotDeletedComments: Int,
|
||||
@SerializedName("data")
|
||||
val comments: ArrayList<Comment>
|
||||
|
||||
): OverviewRecycleViewItem()
|
@ -35,7 +35,7 @@ class Video(
|
||||
var licence: Licence,
|
||||
var language: Language,
|
||||
var nsfw: Boolean,
|
||||
var description: String,
|
||||
var description: String? = null,
|
||||
var local: Boolean,
|
||||
var live: Boolean,
|
||||
var duration: Int,
|
||||
@ -66,7 +66,7 @@ class Video(
|
||||
companion object {
|
||||
|
||||
@JvmStatic
|
||||
fun getMediaDescription(context: Context?, video: Video): MediaDescriptionCompat {
|
||||
fun getMediaDescription(video: Video): MediaDescriptionCompat {
|
||||
|
||||
// String apiBaseURL = APIUrlHelper.getUrlWithVersion(context);
|
||||
|
||||
|
@ -0,0 +1,7 @@
|
||||
package net.schueller.peertube.model.ui
|
||||
|
||||
import net.schueller.peertube.model.Video
|
||||
|
||||
class VideoMetaViewItem: OverviewRecycleViewItem() {
|
||||
var video: Video? = null
|
||||
}
|
@ -16,15 +16,23 @@
|
||||
*/
|
||||
package net.schueller.peertube.network;
|
||||
|
||||
import com.google.gson.JsonObject;
|
||||
|
||||
import net.schueller.peertube.model.Account;
|
||||
import net.schueller.peertube.model.Channel;
|
||||
import net.schueller.peertube.model.ChannelList;
|
||||
import net.schueller.peertube.model.Me;
|
||||
import net.schueller.peertube.model.VideoList;
|
||||
|
||||
import okhttp3.RequestBody;
|
||||
import okhttp3.ResponseBody;
|
||||
import retrofit2.Call;
|
||||
import retrofit2.http.Body;
|
||||
import retrofit2.http.DELETE;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.POST;
|
||||
import retrofit2.http.PUT;
|
||||
import retrofit2.http.Path;
|
||||
import retrofit2.http.Query;
|
||||
|
||||
@ -52,5 +60,18 @@ public interface GetUserService {
|
||||
@Path(value = "displayName", encoded = true) String displayName
|
||||
);
|
||||
|
||||
@GET("users/me/subscriptions/exist")
|
||||
Call<JsonObject> subscriptionsExist(
|
||||
@Query("uris") String videoChannelNameAndHost
|
||||
);
|
||||
|
||||
@POST("users/me/subscriptions")
|
||||
Call<ResponseBody> subscribe(
|
||||
@Body RequestBody params
|
||||
);
|
||||
|
||||
@DELETE("users/me/subscriptions/{videoChannelNameAndHost}")
|
||||
Call<ResponseBody> unsubscribe(
|
||||
@Path(value = "videoChannelNameAndHost", encoded = true) String videoChannelNameAndHost
|
||||
);
|
||||
}
|
@ -16,6 +16,7 @@
|
||||
*/
|
||||
package net.schueller.peertube.network;
|
||||
|
||||
import net.schueller.peertube.model.CommentThread;
|
||||
import net.schueller.peertube.model.Description;
|
||||
import net.schueller.peertube.model.Overview;
|
||||
import net.schueller.peertube.model.Rating;
|
||||
@ -91,4 +92,12 @@ public interface GetVideoDataService {
|
||||
@Query("page") int page
|
||||
);
|
||||
|
||||
// https://troll.tv/api/v1/videos/{id}/comment-threads?start=0&count=10&sort=-createdAt
|
||||
@GET("videos/{id}/comment-threads")
|
||||
Call<CommentThread> getCommentThreads(
|
||||
@Path(value = "id", encoded = true) Integer id,
|
||||
@Query("start") int start,
|
||||
@Query("count") int count,
|
||||
@Query("sort") String sort
|
||||
);
|
||||
}
|
@ -1,358 +0,0 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.service;
|
||||
|
||||
import static android.media.session.PlaybackState.ACTION_PAUSE;
|
||||
import static android.media.session.PlaybackState.ACTION_PLAY;
|
||||
import static com.google.android.exoplayer2.ui.PlayerNotificationManager.ACTION_STOP;
|
||||
import static net.schueller.peertube.activity.VideoListActivity.EXTRA_VIDEOID;
|
||||
import static net.schueller.peertube.network.UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder;
|
||||
|
||||
import android.app.Notification;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.Service;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.graphics.Bitmap;
|
||||
import android.media.AudioManager;
|
||||
import android.net.Uri;
|
||||
import android.os.Binder;
|
||||
import android.os.IBinder;
|
||||
import android.preference.PreferenceManager;
|
||||
import android.support.v4.media.MediaDescriptionCompat;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.util.Log;
|
||||
import android.webkit.URLUtil;
|
||||
import android.widget.Toast;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.exoplayer2.C;
|
||||
import com.google.android.exoplayer2.MediaItem;
|
||||
import com.google.android.exoplayer2.PlaybackParameters;
|
||||
import com.google.android.exoplayer2.Player;
|
||||
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes;
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector;
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator;
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSourceFactory;
|
||||
import com.google.android.exoplayer2.source.MediaSource;
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager;
|
||||
import com.google.android.exoplayer2.upstream.DataSource;
|
||||
import com.google.android.exoplayer2.util.Util;
|
||||
import net.schueller.peertube.R;
|
||||
import net.schueller.peertube.activity.VideoPlayActivity;
|
||||
import net.schueller.peertube.helper.APIUrlHelper;
|
||||
import net.schueller.peertube.helper.MetaDataHelper;
|
||||
import net.schueller.peertube.model.Video;
|
||||
import okhttp3.OkHttpClient;
|
||||
|
||||
public class VideoPlayerService extends Service {
|
||||
|
||||
private static final String TAG = "VideoPlayerService";
|
||||
|
||||
private static final String MEDIA_SESSION_TAG = "peertube_player";
|
||||
|
||||
private final IBinder mBinder = new LocalBinder();
|
||||
|
||||
private static final String PLAYBACK_CHANNEL_ID = "playback_channel";
|
||||
|
||||
private static final Integer PLAYBACK_NOTIFICATION_ID = 1;
|
||||
|
||||
public SimpleExoPlayer player;
|
||||
|
||||
private Video currentVideo;
|
||||
|
||||
private String currentStreamUrl;
|
||||
|
||||
private boolean currentStreamUrlIsHLS;
|
||||
|
||||
private PlayerNotificationManager playerNotificationManager;
|
||||
|
||||
private IntentFilter becomeNoisyIntentFilter = new IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY);
|
||||
|
||||
private BecomingNoisyReceiver myNoisyAudioStreamReceiver = new BecomingNoisyReceiver();
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
Log.v(TAG, "onCreate...");
|
||||
|
||||
super.onCreate();
|
||||
|
||||
player = new SimpleExoPlayer.Builder(getApplicationContext())
|
||||
.setTrackSelector(new DefaultTrackSelector(getApplicationContext()))
|
||||
.build();
|
||||
|
||||
// Stop player if audio device changes, e.g. headphones unplugged
|
||||
player.addListener(new Player.EventListener() {
|
||||
@Override
|
||||
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
|
||||
|
||||
if (playbackState == ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
|
||||
Log.v(TAG, "ACTION_PLAY: " + playbackState);
|
||||
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter);
|
||||
}
|
||||
|
||||
if (playbackState
|
||||
== ACTION_PLAY) { // this means that play is available, hence the audio is paused or stopped
|
||||
Log.v(TAG, "ACTION_PAUSE: " + playbackState);
|
||||
safeUnregisterReceiver();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
public class LocalBinder extends Binder {
|
||||
|
||||
public VideoPlayerService getService() {
|
||||
// Return this instance of VideoPlayerService so clients can call public methods
|
||||
return VideoPlayerService.this;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
public void onDestroy() {
|
||||
|
||||
Log.v(TAG, "onDestroy...");
|
||||
if (playerNotificationManager != null) {
|
||||
playerNotificationManager.setPlayer(null);
|
||||
}
|
||||
//Was seeing an error when exiting the program about not unregistering the receiver.
|
||||
safeUnregisterReceiver();
|
||||
|
||||
if (player != null) {
|
||||
player.release();
|
||||
player = null;
|
||||
}
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
private void safeUnregisterReceiver()
|
||||
{
|
||||
try {
|
||||
unregisterReceiver(myNoisyAudioStreamReceiver);
|
||||
} catch (Exception e) {
|
||||
Log.e("VideoPlayerService", "attempted to unregister a nonregistered service");
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public IBinder onBind(Intent intent) {
|
||||
return mBinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int onStartCommand(Intent intent, int flags, int startId) {
|
||||
Context context = this;
|
||||
Log.v(TAG, "onStartCommand...");
|
||||
|
||||
if (!URLUtil.isValidUrl(currentStreamUrl)) {
|
||||
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show();
|
||||
return START_NOT_STICKY;
|
||||
} else {
|
||||
playVideo();
|
||||
return START_STICKY;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void setCurrentVideo(Video video) {
|
||||
Log.v(TAG, "setCurrentVideo...");
|
||||
currentVideo = video;
|
||||
}
|
||||
|
||||
public void setCurrentStreamUrl(String streamUrl, boolean isHLS) {
|
||||
Log.v(TAG, "setCurrentStreamUrl..." + streamUrl);
|
||||
currentStreamUrlIsHLS = isHLS;
|
||||
currentStreamUrl = streamUrl;
|
||||
}
|
||||
|
||||
//Playback speed control
|
||||
public void setPlayBackSpeed(float speed) {
|
||||
Log.v(TAG, "setPlayBackSpeed...");
|
||||
player.setPlaybackParameters(new PlaybackParameters(speed));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current playback speed of the player.
|
||||
*
|
||||
* @return the current playback speed of the player.
|
||||
*/
|
||||
public float getPlayBackSpeed() {
|
||||
return player.getPlaybackParameters().speed;
|
||||
}
|
||||
|
||||
public void playVideo() {
|
||||
Context context = this;
|
||||
|
||||
// We need a valid URL
|
||||
|
||||
Log.v(TAG, "playVideo...");
|
||||
|
||||
// Produces DataSource instances through which media data is loaded.
|
||||
// DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(),
|
||||
// Util.getUserAgent(getApplicationContext(), "PeerTube"), null);
|
||||
|
||||
OkHttpClient.Builder okhttpClientBuilder;
|
||||
|
||||
if (!APIUrlHelper.useInsecureConnection(this)) {
|
||||
okhttpClientBuilder = new OkHttpClient.Builder();
|
||||
} else {
|
||||
okhttpClientBuilder = getUnsafeOkHttpClientBuilder();
|
||||
}
|
||||
|
||||
// Create a data source factory.
|
||||
DataSource.Factory dataSourceFactory = new OkHttpDataSourceFactory(okhttpClientBuilder.build(), Util.getUserAgent(getApplicationContext(), "PeerTube"));
|
||||
|
||||
// Create a progressive media source pointing to a stream uri.
|
||||
MediaSource mediaSource;
|
||||
if (currentStreamUrlIsHLS) {
|
||||
mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
|
||||
} else {
|
||||
mediaSource = new ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)));
|
||||
}
|
||||
|
||||
// Set the media source to be played.
|
||||
player.setMediaSource(mediaSource);
|
||||
|
||||
// Prepare the player.
|
||||
player.prepare();
|
||||
|
||||
// Auto play
|
||||
player.setPlayWhenReady(true);
|
||||
|
||||
//set playback speed to global default
|
||||
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
|
||||
float speed = Float.parseFloat(sharedPref.getString(getString(R.string.pref_video_speed_key), "1.0"));
|
||||
|
||||
this.setPlayBackSpeed(speed);
|
||||
|
||||
playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel(
|
||||
context, PLAYBACK_CHANNEL_ID, R.string.playback_channel_name,
|
||||
PLAYBACK_NOTIFICATION_ID,
|
||||
new PlayerNotificationManager.MediaDescriptionAdapter() {
|
||||
|
||||
@Override
|
||||
public String getCurrentContentTitle(Player player) {
|
||||
return currentVideo.getName();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public PendingIntent createCurrentContentIntent(Player player) {
|
||||
Intent intent = new Intent(context, VideoPlayActivity.class);
|
||||
intent.putExtra(EXTRA_VIDEOID, currentVideo.getUuid());
|
||||
return PendingIntent.getActivity(context, 0, intent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getCurrentContentText(Player player) {
|
||||
return MetaDataHelper.getMetaString(
|
||||
currentVideo.getCreatedAt(),
|
||||
currentVideo.getViews(),
|
||||
getBaseContext()
|
||||
);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Bitmap getCurrentLargeIcon(Player player,
|
||||
PlayerNotificationManager.BitmapCallback callback) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
playerNotificationManager.setSmallIcon(R.drawable.ic_logo_bw);
|
||||
|
||||
// don't show skip buttons in notification
|
||||
playerNotificationManager.setUseNavigationActions(false);
|
||||
playerNotificationManager.setUseStopAction(true);
|
||||
|
||||
playerNotificationManager.setNotificationListener(
|
||||
new PlayerNotificationManager.NotificationListener() {
|
||||
@Override
|
||||
public void onNotificationStarted(int notificationId, Notification notification) {
|
||||
startForeground(notificationId, notification);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNotificationCancelled(int notificationId) {
|
||||
Log.v(TAG, "onNotificationCancelled...");
|
||||
stopForeground(true);
|
||||
Intent killFloat = new Intent(ACTION_STOP);
|
||||
sendBroadcast(killFloat);
|
||||
/*
|
||||
Intent killFloat = new Intent(BROADCAST_ACTION);
|
||||
Intent killFloatingWindow = new Intent(getApplicationContext(),VideoPlayActivity.class);
|
||||
killFloatingWindow.putExtra("killFloat",true);
|
||||
|
||||
startActivity(killFloatingWindow);
|
||||
// TODO: only kill the notification if we no longer have a bound activity
|
||||
stopForeground(true);
|
||||
*/
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
playerNotificationManager.setPlayer(player);
|
||||
|
||||
// external Media control, Android Wear / Google Home etc.
|
||||
MediaSessionCompat mediaSession = new MediaSessionCompat(context, MEDIA_SESSION_TAG);
|
||||
mediaSession.setActive(true);
|
||||
playerNotificationManager.setMediaSessionToken(mediaSession.getSessionToken());
|
||||
MediaSessionConnector mediaSessionConnector = new MediaSessionConnector(mediaSession);
|
||||
mediaSessionConnector.setQueueNavigator(new TimelineQueueNavigator(mediaSession) {
|
||||
@Override
|
||||
public MediaDescriptionCompat getMediaDescription(Player player, int windowIndex) {
|
||||
return Video.getMediaDescription(context, currentVideo);
|
||||
}
|
||||
});
|
||||
mediaSessionConnector.setPlayer(player);
|
||||
|
||||
// Audio Focus
|
||||
AudioAttributes audioAttributes = new AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.CONTENT_TYPE_MOVIE)
|
||||
.build();
|
||||
player.setAudioAttributes(audioAttributes, true);
|
||||
|
||||
}
|
||||
|
||||
// pause playback on audio output change
|
||||
private class BecomingNoisyReceiver extends BroadcastReceiver {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY.equals(intent.getAction())) {
|
||||
player.setPlayWhenReady(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
@ -0,0 +1,312 @@
|
||||
/*
|
||||
* Copyright (C) 2020 Stefan Schüller <sschueller@techdroid.com>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package net.schueller.peertube.service
|
||||
|
||||
import android.app.Notification
|
||||
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
|
||||
import net.schueller.peertube.model.Video.Companion.getMediaDescription
|
||||
import android.os.IBinder
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager
|
||||
import android.content.IntentFilter
|
||||
import android.media.AudioManager
|
||||
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector
|
||||
import android.media.session.PlaybackState
|
||||
import android.content.Intent
|
||||
import android.widget.Toast
|
||||
import net.schueller.peertube.helper.APIUrlHelper
|
||||
import net.schueller.peertube.network.UnsafeOkHttpClient
|
||||
import com.google.android.exoplayer2.source.MediaSource
|
||||
import com.google.android.exoplayer2.source.hls.HlsMediaSource
|
||||
import com.google.android.exoplayer2.source.ProgressiveMediaSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager.MediaDescriptionAdapter
|
||||
import android.app.PendingIntent
|
||||
import android.app.Service
|
||||
import net.schueller.peertube.activity.VideoPlayActivity
|
||||
import net.schueller.peertube.activity.VideoListActivity
|
||||
import android.graphics.Bitmap
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import com.google.android.exoplayer2.ext.mediasession.MediaSessionConnector
|
||||
import com.google.android.exoplayer2.ext.mediasession.TimelineQueueNavigator
|
||||
import android.support.v4.media.MediaDescriptionCompat
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import android.os.Binder
|
||||
import android.util.Log
|
||||
import android.webkit.URLUtil
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.google.android.exoplayer2.*
|
||||
import com.google.android.exoplayer2.audio.AudioAttributes
|
||||
import com.google.android.exoplayer2.ext.okhttp.OkHttpDataSource
|
||||
import com.google.android.exoplayer2.ui.PlayerNotificationManager.NotificationListener
|
||||
import net.schueller.peertube.R.drawable
|
||||
import net.schueller.peertube.R.string
|
||||
import net.schueller.peertube.model.Video
|
||||
import java.lang.Exception
|
||||
|
||||
class VideoPlayerService : Service() {
|
||||
|
||||
private val mBinder: IBinder = LocalBinder()
|
||||
@JvmField
|
||||
var player: ExoPlayer? = null
|
||||
private var currentVideo: Video? = null
|
||||
private var currentStreamUrl: String? = null
|
||||
private var currentStreamUrlIsHLS = false
|
||||
private var playerNotificationManager: PlayerNotificationManager? = null
|
||||
private val becomeNoisyIntentFilter = IntentFilter(AudioManager.ACTION_AUDIO_BECOMING_NOISY)
|
||||
private val myNoisyAudioStreamReceiver = BecomingNoisyReceiver()
|
||||
override fun onCreate() {
|
||||
Log.v(TAG, "onCreate...")
|
||||
super.onCreate()
|
||||
player = ExoPlayer.Builder(applicationContext)
|
||||
.setTrackSelector(DefaultTrackSelector(applicationContext))
|
||||
.build()
|
||||
|
||||
// Stop player if audio device changes, e.g. headphones unplugged
|
||||
player!!.addListener(object : Player.Listener {
|
||||
override fun onPlayerStateChanged(playWhenReady: Boolean, playbackState: Int) {
|
||||
if (playbackState.toLong() == PlaybackState.ACTION_PAUSE) { // this means that pause is available, hence the audio is playing
|
||||
Log.v(TAG, "ACTION_PLAY: $playbackState")
|
||||
registerReceiver(myNoisyAudioStreamReceiver, becomeNoisyIntentFilter)
|
||||
}
|
||||
if (playbackState
|
||||
.toLong() == PlaybackState.ACTION_PLAY
|
||||
) { // this means that play is available, hence the audio is paused or stopped
|
||||
Log.v(TAG, "ACTION_PAUSE: $playbackState")
|
||||
safeUnregisterReceiver()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
inner class LocalBinder : Binder() {
|
||||
|
||||
// Return this instance of VideoPlayerService so clients can call public methods
|
||||
val service: VideoPlayerService
|
||||
get() =// Return this instance of VideoPlayerService so clients can call public methods
|
||||
this@VideoPlayerService
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
Log.v(TAG, "onDestroy...")
|
||||
if (playerNotificationManager != null) {
|
||||
playerNotificationManager!!.setPlayer(null)
|
||||
}
|
||||
//Was seeing an error when exiting the program about not unregistering the receiver.
|
||||
safeUnregisterReceiver()
|
||||
if (player != null) {
|
||||
player!!.release()
|
||||
player = null
|
||||
}
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
private fun safeUnregisterReceiver() {
|
||||
try {
|
||||
unregisterReceiver(myNoisyAudioStreamReceiver)
|
||||
} catch (e: Exception) {
|
||||
Log.e("VideoPlayerService", "attempted to unregister a non-registered service")
|
||||
}
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent): IBinder {
|
||||
return mBinder
|
||||
}
|
||||
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
val context: Context = this
|
||||
Log.v(TAG, "onStartCommand...")
|
||||
return if (!URLUtil.isValidUrl(currentStreamUrl)) {
|
||||
Toast.makeText(context, "Invalid URL provided. Unable to play video.", Toast.LENGTH_SHORT).show()
|
||||
START_NOT_STICKY
|
||||
} else {
|
||||
playVideo()
|
||||
START_STICKY
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentVideo(video: Video?) {
|
||||
Log.v(TAG, "setCurrentVideo...")
|
||||
currentVideo = video
|
||||
}
|
||||
|
||||
fun setCurrentStreamUrl(streamUrl: String, isHLS: Boolean) {
|
||||
Log.v(TAG, "setCurrentStreamUrl...$streamUrl")
|
||||
currentStreamUrlIsHLS = isHLS
|
||||
currentStreamUrl = streamUrl
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the current playback speed of the player.
|
||||
*
|
||||
* @return the current playback speed of the player.
|
||||
*///Playback speed control
|
||||
var playBackSpeed: Float
|
||||
get() = player!!.playbackParameters.speed
|
||||
set(speed) {
|
||||
Log.v(TAG, "setPlayBackSpeed...")
|
||||
player!!.playbackParameters = PlaybackParameters(speed)
|
||||
}
|
||||
|
||||
private fun playVideo() {
|
||||
val context: Context = this
|
||||
|
||||
// We need a valid URL
|
||||
Log.v(TAG, "playVideo...")
|
||||
|
||||
// Produces DataSource instances through which media data is loaded.
|
||||
val okhttpClientBuilder: okhttp3.OkHttpClient.Builder = if (!APIUrlHelper.useInsecureConnection(this)) {
|
||||
okhttp3.OkHttpClient.Builder()
|
||||
} else {
|
||||
UnsafeOkHttpClient.getUnsafeOkHttpClientBuilder()
|
||||
}
|
||||
|
||||
// Create a data source factory.
|
||||
val dataSourceFactory: OkHttpDataSource.Factory = OkHttpDataSource.Factory(
|
||||
okhttpClientBuilder.build()
|
||||
)
|
||||
|
||||
// Create a progressive media source pointing to a stream uri.
|
||||
val mediaSource: MediaSource = if (currentStreamUrlIsHLS) {
|
||||
HlsMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
|
||||
} else {
|
||||
ProgressiveMediaSource.Factory(dataSourceFactory)
|
||||
.createMediaSource(MediaItem.fromUri(Uri.parse(currentStreamUrl)))
|
||||
}
|
||||
|
||||
// Set the media source to be played.
|
||||
player!!.setMediaSource(mediaSource)
|
||||
|
||||
// Prepare the player.
|
||||
player!!.prepare()
|
||||
|
||||
// Auto play
|
||||
player!!.playWhenReady = true
|
||||
|
||||
//set playback speed to global default
|
||||
val sharedPref = getSharedPreferences(
|
||||
packageName + "_preferences",
|
||||
Context.MODE_PRIVATE
|
||||
)
|
||||
|
||||
val speed = sharedPref.getString(getString(string.pref_video_speed_key), "1.0")!!.toFloat()
|
||||
playBackSpeed = speed
|
||||
|
||||
playerNotificationManager = PlayerNotificationManager.Builder(
|
||||
this,
|
||||
PLAYBACK_NOTIFICATION_ID,
|
||||
PLAYBACK_CHANNEL_ID,
|
||||
).setMediaDescriptionAdapter(
|
||||
object : MediaDescriptionAdapter {
|
||||
override fun getCurrentContentTitle(player: Player): CharSequence {
|
||||
return currentVideo!!.name
|
||||
}
|
||||
|
||||
override fun createCurrentContentIntent(player: Player): PendingIntent? {
|
||||
val intent = Intent(context, VideoPlayActivity::class.java)
|
||||
intent.putExtra(VideoListActivity.EXTRA_VIDEOID, currentVideo!!.uuid)
|
||||
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
|
||||
|
||||
}
|
||||
|
||||
override fun getCurrentContentText(player: Player): CharSequence {
|
||||
return getMetaString(
|
||||
currentVideo!!.createdAt,
|
||||
currentVideo!!.views,
|
||||
baseContext
|
||||
)
|
||||
}
|
||||
|
||||
override fun getCurrentSubText(player: Player): CharSequence { return ""}
|
||||
|
||||
override fun getCurrentLargeIcon(
|
||||
player: Player,
|
||||
callback: PlayerNotificationManager.BitmapCallback
|
||||
): Bitmap? {
|
||||
return null
|
||||
}
|
||||
}
|
||||
).setNotificationListener(
|
||||
object : NotificationListener {
|
||||
|
||||
override fun onNotificationPosted(notificationId: Int, notification: Notification, ongoing: Boolean) {
|
||||
super.onNotificationPosted(notificationId, notification, ongoing)
|
||||
if (ongoing) // allow notification to be dismissed if player is stopped
|
||||
startForeground(notificationId, notification)
|
||||
else
|
||||
stopForeground(false)
|
||||
}
|
||||
|
||||
override fun onNotificationCancelled(notificationId: Int, dismissedByUser: Boolean) {
|
||||
super.onNotificationCancelled(notificationId, dismissedByUser)
|
||||
stopSelf()
|
||||
stopForeground(true)
|
||||
}
|
||||
}
|
||||
).setChannelNameResourceId(string.playback_channel_name)
|
||||
.setChannelDescriptionResourceId(string.playback_channel_description)
|
||||
.build()
|
||||
|
||||
playerNotificationManager!!.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
|
||||
playerNotificationManager!!.setSmallIcon(drawable.ic_logo_bw)
|
||||
|
||||
// don't show skip buttons in notification
|
||||
playerNotificationManager!!.setUseNextAction(false)
|
||||
playerNotificationManager!!.setUsePreviousAction(false)
|
||||
|
||||
playerNotificationManager!!.setPlayer(player)
|
||||
|
||||
// external Media control, Android Wear / Google Home etc.
|
||||
val mediaSession = MediaSessionCompat(context, MEDIA_SESSION_TAG)
|
||||
mediaSession.isActive = true
|
||||
playerNotificationManager!!.setMediaSessionToken(mediaSession.sessionToken)
|
||||
val mediaSessionConnector = MediaSessionConnector(mediaSession)
|
||||
mediaSessionConnector.setQueueNavigator(object : TimelineQueueNavigator(mediaSession) {
|
||||
override fun getMediaDescription(player: Player, windowIndex: Int): MediaDescriptionCompat {
|
||||
return getMediaDescription(currentVideo!!)
|
||||
}
|
||||
})
|
||||
mediaSessionConnector.setPlayer(player)
|
||||
|
||||
// Audio Focus
|
||||
val audioAttributes = AudioAttributes.Builder()
|
||||
.setUsage(C.USAGE_MEDIA)
|
||||
.setContentType(C.CONTENT_TYPE_MOVIE)
|
||||
.build()
|
||||
player!!.setAudioAttributes(audioAttributes, true)
|
||||
}
|
||||
|
||||
// pause playback on audio output change
|
||||
private inner class BecomingNoisyReceiver : BroadcastReceiver() {
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (AudioManager.ACTION_AUDIO_BECOMING_NOISY == intent.action) {
|
||||
player!!.playWhenReady = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val TAG = "VideoPlayerService"
|
||||
private const val MEDIA_SESSION_TAG = "peertube_player"
|
||||
private const val PLAYBACK_CHANNEL_ID = "playback_channel"
|
||||
private const val PLAYBACK_NOTIFICATION_ID = 1
|
||||
}
|
||||
}
|
13
app/src/main/res/drawable/ic_chevron_down.xml
Normal file
13
app/src/main/res/drawable/ic_chevron_down.xml
Normal 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>
|
20
app/src/main/res/drawable/ic_close.xml
Normal file
20
app/src/main/res/drawable/ic_close.xml
Normal 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>
|
27
app/src/main/res/drawable/ic_download.xml
Normal file
27
app/src/main/res/drawable/ic_download.xml
Normal 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>
|
11
app/src/main/res/drawable/ic_fast_forward.xml
Normal file
11
app/src/main/res/drawable/ic_fast_forward.xml
Normal 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>
|
20
app/src/main/res/drawable/ic_flag.xml
Normal file
20
app/src/main/res/drawable/ic_flag.xml
Normal 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>
|
10
app/src/main/res/drawable/ic_pause.xml
Normal file
10
app/src/main/res/drawable/ic_pause.xml
Normal 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>
|
7
app/src/main/res/drawable/ic_play.xml
Normal file
7
app/src/main/res/drawable/ic_play.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_playlist_add.xml
Normal file
5
app/src/main/res/drawable/ic_playlist_add.xml
Normal 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>
|
11
app/src/main/res/drawable/ic_rewind.xml
Normal file
11
app/src/main/res/drawable/ic_rewind.xml
Normal 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>
|
41
app/src/main/res/drawable/ic_share_2.xml
Normal file
41
app/src/main/res/drawable/ic_share_2.xml
Normal 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>
|
20
app/src/main/res/drawable/ic_slash.xml
Normal file
20
app/src/main/res/drawable/ic_slash.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_thumbs_down.xml
Normal file
13
app/src/main/res/drawable/ic_thumbs_down.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_thumbs_down_filled.xml
Normal file
13
app/src/main/res/drawable/ic_thumbs_down_filled.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_thumbs_up.xml
Normal file
13
app/src/main/res/drawable/ic_thumbs_up.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_thumbs_up_filled.xml
Normal file
13
app/src/main/res/drawable/ic_thumbs_up_filled.xml
Normal 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>
|
@ -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>
|
||||
|
225
app/src/main/res/layout/fragment_video_description.xml
Normal file
225
app/src/main/res/layout/fragment_video_description.xml
Normal 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>
|
@ -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>
|
94
app/src/main/res/layout/item_video_comments_overview.xml
Normal file
94
app/src/main/res/layout/item_video_comments_overview.xml
Normal 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>
|
402
app/src/main/res/layout/item_video_meta.xml
Normal file
402
app/src/main/res/layout/item_video_meta.xml
Normal 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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -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>
|
@ -64,6 +64,7 @@
|
||||
<string name="title_activity_video_play" translatable="false">VideoPlayActivity</string>
|
||||
|
||||
<string name="playback_channel_name" translatable="false">PeerTube</string>
|
||||
<string name="playback_channel_description" translatable="false">playback_channel</string>
|
||||
|
||||
<string name="peertube_instance_search_default_description" translatable="false">PeerTube, a federated (ActivityPub) video streaming platform using P2P (BitTorrent) directly in the web browser with WebTorrent and Angular.</string>
|
||||
|
||||
|
@ -366,4 +366,14 @@
|
||||
<string name="video_list_live_marker">LIVE</string>
|
||||
<string name="video_get_full_description_failed">Getting full video description failed</string>
|
||||
<string name="video_description_read_more">Read More</string>
|
||||
<string name="player_time_seperator">/</string>
|
||||
<string name="video_meta_show_description">Show Description</string>
|
||||
<string name="video_meta_title_description">Description</string>
|
||||
<string name="video_add_to_playlist">Save</string>
|
||||
<string name="video_block">Block</string>
|
||||
<string name="video_flag">Flag</string>
|
||||
<string name="video_feature_not_yet_implemented">This feature has not yet been implemented. Coming soon!</string>
|
||||
<string name="subscribe">Subscribe</string>
|
||||
<string name="unsubscribe">Unsubscribe</string>
|
||||
<string name="video_comments_title">Comments</string>
|
||||
</resources>
|
@ -76,12 +76,12 @@
|
||||
app:title="@string/pref_background_behavior"
|
||||
app:iconSpaceReserved="false"/>
|
||||
|
||||
<SwitchPreference
|
||||
app:defaultValue="false"
|
||||
app:key="@string/pref_torrent_player_key"
|
||||
app:summary="@string/pref_description_torrent_player"
|
||||
app:title="@string/pref_title_torrent_player"
|
||||
app:iconSpaceReserved="false"/>
|
||||
<!-- <SwitchPreference-->
|
||||
<!-- app:defaultValue="false"-->
|
||||
<!-- app:key="@string/pref_torrent_player_key"-->
|
||||
<!-- app:summary="@string/pref_description_torrent_player"-->
|
||||
<!-- app:title="@string/pref_title_torrent_player"-->
|
||||
<!-- app:iconSpaceReserved="false"/>-->
|
||||
|
||||
</PreferenceCategory>
|
||||
|
||||
|
@ -2,11 +2,11 @@
|
||||
|
||||
buildscript {
|
||||
|
||||
ext.kotlin_version = '1.5.31'
|
||||
ext.kotlin_version = '1.6.10'
|
||||
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
@ -21,7 +21,6 @@ buildscript {
|
||||
allprojects {
|
||||
repositories {
|
||||
google()
|
||||
jcenter()
|
||||
mavenCentral()
|
||||
maven {
|
||||
url 'https://oss.sonatype.org/content/repositories/snapshots'
|
||||
|
@ -1,4 +1,4 @@
|
||||
# 1.1.0 (01-02-2021)
|
||||
# 1.1.0 (2021-02-01)
|
||||
|
||||
|
||||
### Características
|
||||
|
@ -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/
|
||||
|
@ -1 +1 @@
|
||||
Thorium یک پخش کننده PeerTube غیر رسمی است
|
||||
توریوم پخشکنندهای غیررسمی برای پیرتیوب است
|
||||
|
6
fastlane/metadata/android/fr-FR/changelogs/1058.txt
Normal file
6
fastlane/metadata/android/fr-FR/changelogs/1058.txt
Normal 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
|
12
fastlane/metadata/android/fr-FR/changelogs/1059.txt
Normal file
12
fastlane/metadata/android/fr-FR/changelogs/1059.txt
Normal 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
|
@ -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 !
|
||||
|
||||
|
1
fastlane/metadata/android/id/changelogs/1048.txt
Normal file
1
fastlane/metadata/android/id/changelogs/1048.txt
Normal file
@ -0,0 +1 @@
|
||||
- f-droid dikeluarkan untuk memperbaharui deploy otomatis
|
@ -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!
|
||||
|
||||
|
@ -1 +1 @@
|
||||
Thorium adalah unoffical pemutar PeerTube
|
||||
Thorium adalah Aplikasi pemutar media bukan dari PeerTube
|
||||
|
@ -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!
|
||||
|
||||
|
1
fastlane/metadata/android/no-NO/changelogs/1048.txt
Normal file
1
fastlane/metadata/android/no-NO/changelogs/1048.txt
Normal file
@ -0,0 +1 @@
|
||||
- f-droid-utgave for å fikse automatisk utrulling
|
1
fastlane/metadata/android/no-NO/changelogs/1054.txt
Normal file
1
fastlane/metadata/android/no-NO/changelogs/1054.txt
Normal file
@ -0,0 +1 @@
|
||||
- La til støtte for HLS-avspilling
|
1
fastlane/metadata/android/pt-BR/changelogs/1047.txt
Normal file
1
fastlane/metadata/android/pt-BR/changelogs/1047.txt
Normal file
@ -0,0 +1 @@
|
||||
- Atualização de autenticação
|
33
fastlane/metadata/android/pt-BR/full_description.txt
Normal file
33
fastlane/metadata/android/pt-BR/full_description.txt
Normal 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/
|
1
fastlane/metadata/android/pt-BR/video.txt
Normal file
1
fastlane/metadata/android/pt-BR/video.txt
Normal file
@ -0,0 +1 @@
|
||||
https://www.youtube.com/watch?v=PJIsiuSdpq8
|
6
fastlane/metadata/android/pt-PT/changelogs/1058.txt
Normal file
6
fastlane/metadata/android/pt-PT/changelogs/1058.txt
Normal file
@ -0,0 +1,6 @@
|
||||
# 1.2.0 (2021-02-07)
|
||||
|
||||
|
||||
### Funcionalidades
|
||||
|
||||
* Marcar vídeos ao vivo em listas de vídeos 8518b80
|
12
fastlane/metadata/android/pt-PT/changelogs/1059.txt
Normal file
12
fastlane/metadata/android/pt-PT/changelogs/1059.txt
Normal 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
|
11
fastlane/metadata/android/pt-PT/changelogs/1060.txt
Normal file
11
fastlane/metadata/android/pt-PT/changelogs/1060.txt
Normal 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
|
1
fastlane/metadata/android/pt/changelogs/1047.txt
Normal file
1
fastlane/metadata/android/pt/changelogs/1047.txt
Normal file
@ -0,0 +1 @@
|
||||
- Atualização da autenticação
|
1
fastlane/metadata/android/pt/changelogs/1048.txt
Normal file
1
fastlane/metadata/android/pt/changelogs/1048.txt
Normal file
@ -0,0 +1 @@
|
||||
- lançamento f-droid para corrigir a implantação automática
|
7
fastlane/metadata/android/pt/changelogs/1049.txt
Normal file
7
fastlane/metadata/android/pt/changelogs/1049.txt
Normal 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)
|
2
fastlane/metadata/android/pt/changelogs/1050.txt
Normal file
2
fastlane/metadata/android/pt/changelogs/1050.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- adicionado suporte para desativar o SSL
|
||||
- traduções
|
5
fastlane/metadata/android/pt/changelogs/1051.txt
Normal file
5
fastlane/metadata/android/pt/changelogs/1051.txt
Normal 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
|
2
fastlane/metadata/android/pt/changelogs/1052.txt
Normal file
2
fastlane/metadata/android/pt/changelogs/1052.txt
Normal file
@ -0,0 +1,2 @@
|
||||
- servidor de edição implementado no livro do servidor (@kosharskiy)
|
||||
- traduções atualizadas
|
7
fastlane/metadata/android/pt/changelogs/1053.txt
Normal file
7
fastlane/metadata/android/pt/changelogs/1053.txt
Normal 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
|
1
fastlane/metadata/android/pt/changelogs/1054.txt
Normal file
1
fastlane/metadata/android/pt/changelogs/1054.txt
Normal file
@ -0,0 +1 @@
|
||||
- Adicionado suporte a reprodução HLS
|
1
fastlane/metadata/android/pt/changelogs/1055.txt
Normal file
1
fastlane/metadata/android/pt/changelogs/1055.txt
Normal file
@ -0,0 +1 @@
|
||||
- Corrigido modelo incorreto impedindo a reprodução de vídeo
|
6
fastlane/metadata/android/pt/changelogs/1056.txt
Normal file
6
fastlane/metadata/android/pt/changelogs/1056.txt
Normal file
@ -0,0 +1,6 @@
|
||||
# 1.1.0 (2021-02-01)
|
||||
|
||||
|
||||
### Características
|
||||
|
||||
* **lang:** Finlandês adicionado 02bcd74
|
6
fastlane/metadata/android/pt/changelogs/1057.txt
Normal file
6
fastlane/metadata/android/pt/changelogs/1057.txt
Normal 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
|
6
fastlane/metadata/android/pt/changelogs/1058.txt
Normal file
6
fastlane/metadata/android/pt/changelogs/1058.txt
Normal file
@ -0,0 +1,6 @@
|
||||
# 1.2.0 (2021-02-07)
|
||||
|
||||
|
||||
### Características
|
||||
|
||||
* Marcar vídeos ao vivo em listas de vídeo 8518b80
|
12
fastlane/metadata/android/pt/changelogs/1059.txt
Normal file
12
fastlane/metadata/android/pt/changelogs/1059.txt
Normal 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
Loading…
Reference in New Issue
Block a user