Merge pull request #37 from sschueller/develop

Release 1.0.6
This commit is contained in:
Stefan Schüller 2018-12-15 22:03:02 +01:00 committed by GitHub
commit ef92ee51a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 610 additions and 142 deletions

View File

@ -6,8 +6,8 @@ android {
applicationId "net.schueller.peertube"
minSdkVersion 23
targetSdkVersion 28
versionCode 105
versionName "1.0.5"
versionCode 106
versionName "1.0.6"
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
@ -44,7 +44,11 @@ android {
// implementation 'org.webrtc:google-webrtc:1.0.+'
// video player
implementation 'com.google.android.exoplayer:exoplayer:2.8.1'
implementation 'com.google.android.exoplayer:exoplayer-core:2.8.1'
implementation 'com.google.android.exoplayer:exoplayer-dash:2.8.1'
implementation 'com.google.android.exoplayer:exoplayer-ui:2.8.1'
implementation 'com.google.android.exoplayer:exoplayer-hls:2.8.1'
implementation 'com.google.android.exoplayer:exoplayer-smoothstreaming:2.8.1'
// implementation 'com.devbrackets.android:exomedia:4.1.0'
// testing

View File

@ -50,5 +50,6 @@
android:exported="false" />
<activity android:name=".activity.SelectServerActivity"/>
<service android:name=".service.VideoPlayerService" />
</application>
</manifest>

View File

@ -1,20 +1,27 @@
package net.schueller.peertube.activity;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.content.SharedPreferences;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Environment;
import android.os.IBinder;
import android.preference.PreferenceManager;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.PopupMenu;
import android.util.Log;
import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
@ -25,32 +32,21 @@ 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.ExoPlayerFactory;
import com.google.android.exoplayer2.Format;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.decoder.DecoderCounters;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.trackselection.AdaptiveTrackSelection;
import com.google.android.exoplayer2.trackselection.DefaultTrackSelector;
import com.google.android.exoplayer2.trackselection.TrackSelection;
import com.google.android.exoplayer2.trackselection.TrackSelector;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.BandwidthMeter;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultBandwidthMeter;
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import com.google.android.exoplayer2.video.VideoRendererEventListener;
import com.squareup.picasso.Picasso;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.intents.Intents;
import net.schueller.peertube.model.Avatar;
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 retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
@ -61,95 +57,61 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
private ProgressBar progressBar;
private PlayerView simpleExoPlayerView;
private SimpleExoPlayer player;
private Intent videoPlayerIntent;
private Context context = this;
boolean mBound = false;
VideoPlayerService mService;
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;
}
};
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_video_play);
// get video ID
Intent intent = getIntent();
String videoID = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
Log.v(TAG, "click: " + videoID);
progressBar = findViewById(R.id.progress);
progressBar.setMax(100);
// PlayerView videoView = findViewById(R.id.video_view);
simpleExoPlayerView = new PlayerView(this);
simpleExoPlayerView = findViewById(R.id.video_view);
// 1. Create a default TrackSelector
BandwidthMeter bandwidthMeter = new DefaultBandwidthMeter();
TrackSelection.Factory videoTrackSelectionFactory =
new AdaptiveTrackSelection.Factory(bandwidthMeter);
TrackSelector trackSelector =
new DefaultTrackSelector(videoTrackSelectionFactory);
// 2. Create the player
player = ExoPlayerFactory.newSimpleInstance(getApplicationContext(), trackSelector);
simpleExoPlayerView.setPlayer(player);
// get video details from api
String apiBaseURL = APIUrlHelper.getUrlWithVersion(this);
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL).create(GetVideoDataService.class);
Call<Video> call = service.getVideoData(videoID);
call.enqueue(new Callback<Video>() {
@Override
public void onResponse(@NonNull Call<Video> call, @NonNull Response<Video> response) {
// Toast.makeText(TorrentVideoPlayActivity.this, response.body().getDescription(), Toast.LENGTH_SHORT).show();
TextView videoName = findViewById(R.id.name);
TextView videoDescription = findViewById(R.id.description);
TextView videoMeta = findViewById(R.id.videoMeta);
try {
videoName.setText(response.body().getName());
videoDescription.setText(response.body().getDescription());
videoMeta.setText(
MetaDataHelper.getMetaString(
response.body().getCreatedAt(),
response.body().getViews(),
getBaseContext()
)
);
String streamUrl = response.body().getFiles().get(0).getFileUrl();
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (sharedPref.getBoolean("pref_torrent_player",false)) {
streamUrl = response.body().getFiles().get(0).getTorrentUrl();
TorrentStream torrentStream = setupTorrentStream();
torrentStream.startStream(streamUrl);
} else {
setupVideoView(Uri.parse(streamUrl));
}
Log.v(TAG, streamUrl);
} catch (NullPointerException e) {
e.getStackTrace();
}
}
@Override
public void onFailure(@NonNull Call<Video> call, @NonNull Throwable t) {
Log.wtf(TAG, t.fillInStackTrace());
Toast.makeText(VideoPlayActivity.this, "Something went wrong...Please try later!", Toast.LENGTH_SHORT).show();
}
});
videoPlayerIntent = new Intent(this, VideoPlayerService.class);
bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE);
}
private void startPlayer()
{
Util.startForegroundService(context, videoPlayerIntent);
}
/**
* Torrent Playback
*
* @return torrent stream
*/
private TorrentStream setupTorrentStream() {
TorrentOptions torrentOptions = new TorrentOptions.Builder()
@ -163,9 +125,8 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
@Override
public void onStreamReady(Torrent torrent) {
Log.d(TAG, "Ready");
setupVideoView(Uri.fromFile(torrent.getVideoFile()));
mService.setCurrentStreamUrl(Uri.fromFile(torrent.getVideoFile()).toString());
startPlayer();
}
@Override
@ -201,28 +162,12 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
return torrentStream;
}
private void setupVideoView(Uri videoStream) {
Log.d(TAG, "Play Video");
// Produces DataSource instances through which media data is loaded.
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(),
Util.getUserAgent(getApplicationContext(), "PeerTube"), null);
// This is the MediaSource representing the media to be played.
MediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(videoStream);
// Prepare the player with the source.
player.prepare(videoSource);
// Auto play
player.setPlayWhenReady(true);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
Log.v(TAG, "onConfigurationChanged()...");
super.onConfigurationChanged(newConfig);
TextView nameView = findViewById(R.id.name);
@ -256,8 +201,105 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
}
}
private void loadVideo()
{
// get video ID
Intent intent = getIntent();
String videoID = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID);
Log.v(TAG, "click: " + videoID);
// get video details from api
String apiBaseURL = APIUrlHelper.getUrlWithVersion(this);
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL).create(GetVideoDataService.class);
Call<Video> call = service.getVideoData(videoID);
call.enqueue(new Callback<Video>() {
@Override
public void onResponse(@NonNull Call<Video> call, @NonNull Response<Video> response) {
// Toast.makeText(TorrentVideoPlayActivity.this, response.body().getDescription(), Toast.LENGTH_SHORT).show();
// TODO: remove this code duplication, similar code as in video list rows
TextView videoName = findViewById(R.id.name);
TextView videoDescription = findViewById(R.id.description);
TextView videoOwner = findViewById(R.id.videoOwner);
TextView videoMeta = findViewById(R.id.videoMeta);
ImageView avatarView = findViewById(R.id.avatar);
ImageButton moreButton = findViewById(R.id.moreButton);
Video video = response.body();
mService.setCurrentVideo(video);
String baseUrl = APIUrlHelper.getUrl(context);
Avatar avatar = video.getAccount().getAvatar();
if (avatar != null) {
String avatarPath = avatar.getPath();
Picasso.with(context)
.load(baseUrl + avatarPath)
.into(avatarView);
}
videoName.setText(video.getName());
videoMeta.setText(
MetaDataHelper.getMetaString(
video.getCreatedAt(),
video.getViews(),
getBaseContext()
)
);
videoOwner.setText(
MetaDataHelper.getOwnerString(video.getAccount().getName(),
video.getAccount().getHost(),
context
)
);
videoDescription.setText(video.getDescription());
moreButton.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(context, v);
popup.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_share:
Intents.Share(context, video);
return true;
default:
return false;
}
});
popup.inflate(R.menu.menu_video_row_mode);
popup.show();
});
mService.setCurrentStreamUrl(video.getFiles().get(0).getFileUrl());
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
if (sharedPref.getBoolean("pref_torrent_player", false)) {
String stream = video.getFiles().get(0).getTorrentUrl();
TorrentStream torrentStream = setupTorrentStream();
torrentStream.startStream(stream);
} else {
startPlayer();
}
}
@Override
public void onFailure(@NonNull Call<Video> call, @NonNull Throwable t) {
Log.wtf(TAG, t.fillInStackTrace());
Toast.makeText(VideoPlayActivity.this, "Something went wrong...Please try later!", Toast.LENGTH_SHORT).show();
}
});
}
@Override
public void onVideoEnabled(DecoderCounters counters) {
Log.v(TAG, "onVideoEnabled()...");
}
@ -288,15 +330,14 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
@Override
public void onVideoDisabled(DecoderCounters counters) {
Log.v(TAG, "onVideoDisabled()...");
}
@Override
protected void onDestroy() {
super.onDestroy();
Log.v(TAG, "onDestroy()...");
player.release();
simpleExoPlayerView.setPlayer(null);
}
@Override
@ -314,6 +355,8 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
@Override
protected void onStop() {
super.onStop();
unbindService(mConnection);
mBound = false;
Log.v(TAG, "onStop()...");
}
@ -322,4 +365,5 @@ public class VideoPlayActivity extends AppCompatActivity implements VideoRendere
super.onStart();
Log.v(TAG, "onStart()...");
}
}

View File

@ -3,10 +3,12 @@ package net.schueller.peertube.adapter;
import android.content.Context;
import android.content.Intent;
import android.support.annotation.NonNull;
import android.support.v7.widget.PopupMenu;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.TextView;
@ -16,6 +18,7 @@ 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.intents.Intents;
import net.schueller.peertube.model.Avatar;
import net.schueller.peertube.model.Video;
@ -28,7 +31,7 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
private ArrayList<Video> videoList;
private Context context;
private String apiBaseURL;
private String baseUrl;
public VideoAdapter(ArrayList<Video> videoList, Context context) {
this.videoList = videoList;
@ -41,7 +44,7 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view = layoutInflater.inflate(R.layout.row_video, parent, false);
apiBaseURL = APIUrlHelper.getUrl(context);
baseUrl = APIUrlHelper.getUrl(context);
return new VideoViewHolder(view);
}
@ -50,7 +53,7 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
public void onBindViewHolder(@NonNull VideoViewHolder holder, int position) {
Picasso.with(this.context)
.load(apiBaseURL + videoList.get(position).getPreviewPath())
.load(baseUrl + videoList.get(position).getPreviewPath())
.into(holder.thumb);
@ -58,7 +61,7 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
if (avatar != null) {
String avatarPath = avatar.getPath();
Picasso.with(this.context)
.load(apiBaseURL + avatarPath)
.load(baseUrl + avatarPath)
.into(holder.avatar);
}
@ -91,6 +94,23 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
});
holder.moreButton.setOnClickListener(v -> {
PopupMenu popup = new PopupMenu(context, v);
popup.setOnMenuItemClickListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.menu_share:
Intents.Share(context, videoList.get(position));
return true;
default:
return false;
}
});
popup.inflate(R.menu.menu_video_row_mode);
popup.show();
});
}
public void setData(ArrayList<Video> data) {
@ -112,6 +132,7 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
TextView name, videoMeta, videoOwner;
ImageView thumb, avatar;
ImageButton moreButton;
View mView;
VideoViewHolder(View itemView) {
@ -121,6 +142,7 @@ public class VideoAdapter extends RecyclerView.Adapter<VideoAdapter.VideoViewHol
avatar = itemView.findViewById(R.id.avatar);
videoMeta = itemView.findViewById(R.id.videoMeta);
videoOwner = itemView.findViewById(R.id.videoOwner);
moreButton = itemView.findViewById(R.id.moreButton);
mView = itemView;
}
}

View File

@ -23,4 +23,8 @@ public class APIUrlHelper{
public static String getUrlWithVersion(Context context) {
return APIUrlHelper.getUrl(context) + "/api/v1/";
}
public static String getShareUrl(Context context, String videoUuid) {
return APIUrlHelper.getUrl(context) + "/videos/watch/" + videoUuid;
}
}

View File

@ -0,0 +1,29 @@
package net.schueller.peertube.intents;
import android.content.Context;
import android.content.Intent;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.model.Video;
public class Intents {
/**
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
*
* @param context context
* @param video video
*/
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);
}
}

View File

@ -0,0 +1,187 @@
package net.schueller.peertube.service;
import android.app.Notification;
import android.app.PendingIntent;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
import android.net.Uri;
import android.os.Binder;
import android.os.IBinder;
import android.support.annotation.Nullable;
import android.util.Log;
import com.google.android.exoplayer2.ExoPlayerFactory;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.ExtractorMediaSource;
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.upstream.DefaultDataSourceFactory;
import com.google.android.exoplayer2.util.Util;
import com.squareup.picasso.Picasso;
import net.schueller.peertube.R;
import net.schueller.peertube.activity.VideoPlayActivity;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.model.Video;
import java.io.IOException;
import retrofit2.http.FormUrlEncoded;
public class VideoPlayerService extends Service {
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 PlayerNotificationManager playerNotificationManager;
@Override
public void onCreate() {
super.onCreate();
Log.v("VideoPlayerService", "onCreate...");
player = ExoPlayerFactory.newSimpleInstance(getApplicationContext(), new DefaultTrackSelector());
}
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("VideoPlayerService", "onDestroy...");
playerNotificationManager.setPlayer(null);
player.release();
player = null;
super.onDestroy();
}
@Nullable
@Override
public IBinder onBind(Intent intent) {
return mBinder;
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Log.v("VideoPlayerService", "onStartCommand...");
playVideo();
return START_STICKY;
}
public void setCurrentVideo(Video video)
{
Log.v("VideoPlayerService", "setCurrentVideo...");
currentVideo = video;
}
public void setCurrentStreamUrl(String streamUrl)
{
Log.v("VideoPlayerService", "setCurrentStreamUrl...");
currentStreamUrl = streamUrl;
}
public void playVideo()
{
Context context = this;
Log.v("VideoPlayerService", "playVideo...");
// Produces DataSource instances through which media data is loaded.
DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(getApplicationContext(),
Util.getUserAgent(getApplicationContext(), "PeerTube"), null);
// This is the MediaSource representing the media to be played.
ExtractorMediaSource videoSource = new ExtractorMediaSource.Factory(dataSourceFactory)
.createMediaSource(Uri.parse(currentStreamUrl));
// Prepare the player with the source.
player.prepare(videoSource);
// Auto play
player.setPlayWhenReady(true);
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);
return PendingIntent.getActivity(context, 0, intent,
PendingIntent.FLAG_UPDATE_CURRENT);
}
@Nullable
@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_peertube_bw);
playerNotificationManager.setNotificationListener(
new PlayerNotificationManager.NotificationListener() {
@Override
public void onNotificationStarted(int notificationId, Notification notification) {
startForeground(notificationId, notification);
}
@Override
public void onNotificationCancelled(int notificationId) {
Log.v("VideoPlayerService", "onNotificationCancelled...");
// TODO: only kill the notification if we no longer have a bound activity
stopForeground(true);
}
}
);
playerNotificationManager.setPlayer(player);
}
}

View File

@ -0,0 +1,16 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="16dp"
android:height="22dp"
android:viewportWidth="16"
android:viewportHeight="22">
<path
android:pathData="m0.0336,0.34v10.667l8,-5.333"
android:fillColor="#000000"/>
<path
android:pathData="m0.0336,11.007v10.667l8,-5.333"
android:fillColor="#000000"/>
<path
android:pathData="m8.0336,5.673v10.667l8,-5.333"
android:fillColor="#000000"/>
</vector>

View File

@ -34,32 +34,83 @@
android:indeterminate="false"
android:max="100" />
<android.support.v7.widget.LinearLayoutCompat
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/progress"
android:padding="16dp"
android:padding="6dp"
android:orientation="vertical">
<de.hdodenhof.circleimageview.CircleImageView xmlns:app="http://schemas.android.com/apk/res-auto"
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/name"
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" />
<TextView
android:id="@+id/videoMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/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"
/>
<ImageButton
android:id="@+id/moreButton"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_marginStart="-16dp"
android:layout_marginEnd="0dp"
android:layout_toEndOf="@+id/name"
android:background="@null"
android:contentDescription="@string/descr_overflow_button"
android:src="@drawable/ic_action_more_vert" />
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@+id/videoMeta"
android:layout_alignParentStart="true"
android:layout_marginStart="18dp"
android:layout_marginTop="35dp"
android:layout_marginEnd="12dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Body1" />
</android.support.v7.widget.LinearLayoutCompat>
</RelativeLayout>
</RelativeLayout>

View File

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:layoutDirection="ltr"
android:background="#CC000000"
android:orientation="vertical"
tools:targetApi="28">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center"
android:paddingTop="4dp"
android:orientation="horizontal">
<ImageButton android:id="@id/exo_rew"
style="@style/ExoMediaButton.Rewind"/>
<ImageButton android:id="@id/exo_shuffle"
style="@style/ExoMediaButton.Shuffle"/>
<ImageButton android:id="@id/exo_repeat_toggle"
style="@style/ExoMediaButton"/>
<ImageButton android:id="@id/exo_play"
style="@style/ExoMediaButton.Play"/>
<ImageButton android:id="@id/exo_pause"
style="@style/ExoMediaButton.Pause"/>
<ImageButton android:id="@id/exo_ffwd"
style="@style/ExoMediaButton.FastForward"/>
</LinearLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView android:id="@id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_weight="1"
android:layout_height="26dp"/>
<TextView android:id="@id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textStyle="bold"
android:paddingLeft="4dp"
android:paddingRight="4dp"
android:includeFontPadding="false"
android:textColor="#FFBEBEBE"/>
</LinearLayout>
</LinearLayout>

View File

@ -19,38 +19,45 @@
android:id="@+id/thumb"
android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:maxHeight="300dp"
android:contentDescription="@string/video_row_video_thumbnail"
android:scaleType="fitXY"
android:adjustViewBounds="true"
/>
android:contentDescription="@string/video_row_video_thumbnail"
android:maxHeight="300dp"
android:scaleType="fitXY" />
<de.hdodenhof.circleimageview.CircleImageView
xmlns:app="http://schemas.android.com/apk/res-auto"
<de.hdodenhof.circleimageview.CircleImageView xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/avatar"
android:layout_width="60dp"
android:layout_height="60dp"
android:paddingTop="12dp"
android:paddingEnd="12dp"
android:layout_width="72dp"
android:layout_height="72dp"
android:layout_below="@+id/thumb"
android:layout_alignParentStart="true"
android:layout_marginTop="0dp"
android:contentDescription="@string/video_row_account_avatar"
android:layout_below="@id/thumb"
android:layout_alignParentStart="true"/>
android:paddingStart="12dp"
android:paddingTop="12dp"
android:paddingEnd="12dp" />
<TextView
android:id="@+id/name"
android:paddingTop="12dp"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/thumb"
android:layout_toEndOf="@id/avatar"
android:layout_marginStart="6dp"
android:layout_marginTop="12dp"
android:layout_toEndOf="@+id/avatar"
android:layout_marginEnd="24dp"
android:paddingTop="0dp"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Subhead" />
<TextView
android:id="@+id/videoMeta"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_below="@id/name"
android:layout_toEndOf="@id/avatar"
android:layout_below="@+id/name"
android:layout_marginStart="6dp"
android:layout_marginTop="0dp"
android:layout_marginEnd="6dp"
android:layout_toEndOf="@+id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
<TextView
@ -58,8 +65,27 @@
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="6dp"
android:layout_toEndOf="@id/avatar"
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption" />
android:textAppearance="@style/Base.TextAppearance.AppCompat.Caption"
/>
<ImageButton
android:id="@+id/moreButton"
android:layout_width="wrap_content"
android:layout_height="45dp"
android:layout_below="@+id/thumb"
android:layout_marginStart="-16dp"
android:layout_marginEnd="0dp"
android:layout_toEndOf="@+id/name"
android:background="@null"
android:contentDescription="@string/descr_overflow_button"
android:src="@drawable/ic_action_more_vert" />
</RelativeLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@+id/menu_share"
android:icon="@drawable/ic_action_share"
android:title="@string/menu_share" />
</menu>

View File

@ -49,12 +49,15 @@
<string name="pref_description_show_nsfw">NSFW content will be shown if enabled.</string>
<string name="title_activity_url_video_play">UrlVideoPlayActivity</string>
<string name="pref_title_torrent_player">Torrent Video Player</string>
<string name="pref_description_torrent_player">Video playback via a torrent stream. This requires Storage Permissions.</string>
<string name="pref_description_torrent_player">Video playback via a torrent stream. This requires Storage Permissions. (Alpha, not stable!)</string>
<string name="pref_title_license">License</string>
<string name="pref_description_license">\n<b>GNU Affero General Public License v3.0</b>\n\nPermissions of this strongest copyleft license are conditioned on making available complete source code of licensed works and modifications, which include larger works using a licensed work, under the same license. Copyright and license notices must be preserved. Contributors provide an express grant of patent rights. When a modified version is used to provide a service over a network, the complete source code of the modified version must be made available.</string>
<string name="pref_title_version">Version</string>
<string name="search_hint">Search PeerTube</string>
<string name="title_activity_search">Search</string>
<string name="no_data_available">No Results</string>
<string name="descr_overflow_button">More</string>
<string name="menu_share">Share</string>
<string name="playback_channel_name">PeerTube</string>
</resources>