feat: Merge from temp repo

This commit is contained in:
Stefan Schüller 2022-02-04 14:06:07 +01:00
parent 1de420f0b9
commit 411378f86d
198 changed files with 22 additions and 14646 deletions

View File

@ -1,26 +0,0 @@
package net.schueller.peertube;
import android.content.Context;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
/**
* Instrumented test, which will execute on an Android device.
*
* @see <a href="http://d.android.com/tools/testing">Testing documentation</a>
*/
@RunWith(AndroidJUnit4.class)
public class ExampleInstrumentedTest {
@Test
public void useAppContext() throws Exception {
// Context of the app under test.
Context appContext = InstrumentationRegistry.getTargetContext();
assertEquals("net.schueller.peertube", appContext.getPackageName());
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

View File

@ -1,349 +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.content.Intent;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.google.android.material.bottomnavigation.BottomNavigationView;
import com.google.android.material.bottomnavigation.LabelVisibilityMode;
import com.google.android.material.navigation.NavigationBarView;
import com.mikepenz.iconics.IconicsDrawable;
import com.mikepenz.iconics.typeface.library.fontawesome.FontAwesome;
import com.squareup.picasso.Picasso;
import net.schueller.peertube.R;
import net.schueller.peertube.adapter.ChannelAdapter;
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.helper.MetaDataHelper;
import net.schueller.peertube.model.Account;
import net.schueller.peertube.model.Avatar;
import net.schueller.peertube.model.ChannelList;
import net.schueller.peertube.model.VideoList;
import net.schueller.peertube.network.GetUserService;
import net.schueller.peertube.network.GetVideoDataService;
import net.schueller.peertube.network.RetrofitInstance;
import java.util.ArrayList;
import java.util.Objects;
import java.util.Set;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.coordinatorlayout.widget.CoordinatorLayout;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class AccountActivity extends CommonActivity {
private String TAG = "AccountActivity";
private String apiBaseURL;
private Integer videosStart, videosCount, videosCurrentStart;
private String videosFilter, videosSort, videosNsfw;
private Set<String> videosLanguages;
private ChannelAdapter channelAdapter;
private MultiViewRecycleViewAdapter mMultiViewRecycleViewAdapter;
private RecyclerView recyclerViewVideos;
private RecyclerView recyclerViewChannels;
private SwipeRefreshLayout swipeRefreshLayoutVideos;
private SwipeRefreshLayout swipeRefreshLayoutChannels;
private CoordinatorLayout aboutView;
//private TextView emptyView;
private Boolean isLoadingVideos;
private GetUserService userService;
private String displayNameAndHost;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_account);
apiBaseURL = APIUrlHelper.getUrlWithVersion(this);
userService = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(this)).create(GetUserService.class);
recyclerViewVideos = findViewById(R.id.account_video_recyclerView);
recyclerViewChannels = findViewById(R.id.account_channel_recyclerView);
swipeRefreshLayoutVideos = findViewById(R.id.account_swipeRefreshLayout_videos);
swipeRefreshLayoutChannels = findViewById(R.id.account_swipeRefreshLayout_channels);
aboutView = findViewById(R.id.account_about);
RecyclerView.LayoutManager layoutManagerVideos = new LinearLayoutManager(AccountActivity.this);
recyclerViewVideos.setLayoutManager(layoutManagerVideos);
RecyclerView.LayoutManager layoutManagerVideosChannels = new LinearLayoutManager(AccountActivity.this);
recyclerViewChannels.setLayoutManager(layoutManagerVideosChannels);
mMultiViewRecycleViewAdapter = new MultiViewRecycleViewAdapter();
recyclerViewVideos.setAdapter(mMultiViewRecycleViewAdapter);
channelAdapter = new ChannelAdapter(new ArrayList<>(), AccountActivity.this);
recyclerViewChannels.setAdapter(channelAdapter);
swipeRefreshLayoutVideos.setOnRefreshListener(() -> {
// Refresh items
if (!isLoadingVideos) {
videosCurrentStart = 0;
loadAccountVideos(displayNameAndHost);
}
});
// get video ID
Intent intent = getIntent();
displayNameAndHost = intent.getStringExtra(VideoListActivity.EXTRA_ACCOUNTDISPLAYNAME);
Log.v(TAG, "click: " + displayNameAndHost);
createBottomBarNavigation();
videosStart = 0;
videosCount = 25;
videosCurrentStart = 0;
videosFilter = "";
videosSort = "-publishedAt";
videosNsfw = "";
// Attaching the layout to the toolbar object
Toolbar toolbar = findViewById(R.id.tool_bar_account);
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(toolbar);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
getSupportActionBar().setTitle(displayNameAndHost);
loadAccountVideos(displayNameAndHost);
}
@Override
public boolean onSupportNavigateUp() {
finish(); // close this activity as oppose to navigating up
return false;
}
private void loadAccount(String ownerString) {
// get video details from api
Call<Account> call = userService.getAccount(ownerString);
call.enqueue(new Callback<Account>() {
@Override
public void onResponse(@NonNull Call<Account> call, @NonNull Response<Account> response) {
if (response.isSuccessful()) {
Account account = response.body();
String owner = MetaDataHelper.getOwnerString(account,
AccountActivity.this, true
);
// set view data
TextView ownerStringView = findViewById(R.id.account_owner_string);
ownerStringView.setText(owner);
TextView followers = findViewById(R.id.account_followers);
followers.setText(String.valueOf(account.getFollowersCount()));
TextView description = findViewById(R.id.account_description);
description.setText(account.getDescription());
TextView joined = findViewById(R.id.account_joined);
joined.setText(account.getCreatedAt().toString());
ImageView accountAvatar = findViewById(R.id.account_avatar);
// set Avatar
Avatar avatar = account.getAvatar();
if (avatar != null) {
String avatarPath = avatar.getPath();
Picasso.get()
.load(APIUrlHelper.getUrl(AccountActivity.this) + avatarPath)
.into(accountAvatar);
}
} else {
ErrorHelper.showToastFromCommunicationError( AccountActivity.this, null );
}
}
@Override
public void onFailure(@NonNull Call<Account> call, @NonNull Throwable t) {
Log.wtf(TAG, t.fillInStackTrace());
ErrorHelper.showToastFromCommunicationError( AccountActivity.this, t );
}
});
}
private void loadAccountVideos(String displayNameAndHost) {
isLoadingVideos = false;
GetVideoDataService service = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(this)).create(GetVideoDataService.class);
Call<VideoList> call;
call = service.getAccountVideosData(displayNameAndHost, videosStart, videosCount, videosSort);
call.enqueue(new Callback<VideoList>() {
@Override
public void onResponse(@NonNull Call<VideoList> call, @NonNull Response<VideoList> response) {
Log.v(TAG, response.toString());
if (response.isSuccessful()) {
if (videosCurrentStart == 0) {
mMultiViewRecycleViewAdapter.clearData();
}
if (response.body() != null) {
mMultiViewRecycleViewAdapter.setVideoData(response.body().getVideos());
}
} else{
ErrorHelper.showToastFromCommunicationError( AccountActivity.this, null );
}
isLoadingVideos = false;
swipeRefreshLayoutVideos.setRefreshing(false);
}
@Override
public void onFailure(@NonNull Call<VideoList> call, @NonNull Throwable t) {
Log.wtf("err", t.fillInStackTrace());
ErrorHelper.showToastFromCommunicationError( AccountActivity.this, t );
isLoadingVideos = false;
swipeRefreshLayoutVideos.setRefreshing(false);
}
});
}
private void loadAccountChannels(String displayNameAndHost) {
// get video details from api
Call<ChannelList> call = userService.getAccountChannels(displayNameAndHost);
call.enqueue(new Callback<ChannelList>() {
@Override
public void onResponse(@NonNull Call<ChannelList> call, @NonNull Response<ChannelList> response) {
if (response.isSuccessful()) {
ChannelList channelList = response.body();
} else {
ErrorHelper.showToastFromCommunicationError( AccountActivity.this, null );
}
}
@Override
public void onFailure(@NonNull Call<ChannelList> call, @NonNull Throwable t) {
Log.wtf(TAG, t.fillInStackTrace());
ErrorHelper.showToastFromCommunicationError( AccountActivity.this, t );
}
});
}
private void createBottomBarNavigation() {
// Get Bottom Navigation
BottomNavigationView navigation = findViewById(R.id.account_navigation);
// Always show text label
navigation.setLabelVisibilityMode(NavigationBarView.LABEL_VISIBILITY_LABELED);
// Add Icon font
Menu navMenu = navigation.getMenu();
navMenu.findItem(R.id.account_navigation_about).setIcon(
new IconicsDrawable(this, FontAwesome.Icon.faw_user));
navMenu.findItem(R.id.account_navigation_channels).setIcon(
new IconicsDrawable(this, FontAwesome.Icon.faw_list));
navMenu.findItem(R.id.account_navigation_videos).setIcon(
new IconicsDrawable(this, FontAwesome.Icon.faw_video));
// Click Listener
navigation.setOnNavigationItemSelectedListener(menuItem -> {
switch (menuItem.getItemId()) {
case R.id.account_navigation_about:
swipeRefreshLayoutVideos.setVisibility(View.GONE);
swipeRefreshLayoutChannels.setVisibility(View.GONE);
aboutView.setVisibility(View.VISIBLE);
loadAccount(displayNameAndHost);
return true;
case R.id.account_navigation_channels:
swipeRefreshLayoutVideos.setVisibility(View.GONE);
swipeRefreshLayoutChannels.setVisibility(View.VISIBLE);
aboutView.setVisibility(View.GONE);
loadAccountChannels(displayNameAndHost);
return true;
case R.id.account_navigation_videos:
swipeRefreshLayoutVideos.setVisibility(View.VISIBLE);
swipeRefreshLayoutChannels.setVisibility(View.GONE);
aboutView.setVisibility(View.GONE);
loadAccountVideos(displayNameAndHost);
return true;
}
return false;
});
}
}

View File

@ -1,82 +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.content.SharedPreferences;
import android.content.res.Configuration;
import android.content.res.Resources;
import android.os.Bundle;
import android.preference.PreferenceManager;
import net.schueller.peertube.R;
import java.util.Locale;
import androidx.appcompat.app.AppCompatActivity;
import androidx.appcompat.app.AppCompatDelegate;
public class CommonActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// Set Night Mode
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this);
AppCompatDelegate.setDefaultNightMode(sharedPref.getBoolean(getString(R.string.pref_dark_mode_key), false) ?
AppCompatDelegate.MODE_NIGHT_YES : AppCompatDelegate.MODE_NIGHT_NO);
// Set theme
setTheme(getResources().getIdentifier(
sharedPref.getString(
getString(R.string.pref_theme_key),
getString(R.string.app_default_theme)
),
"style",
getPackageName())
);
// Set language
String countryCode = sharedPref.getString(getString(R.string.pref_language_app_key), null);
if (countryCode == null) {
return;
}
setLocale(countryCode);
}
public void setLocale(String languageCode) {
Locale locale = new Locale(languageCode);
//Neither Chinese language choice was working, found this fix on stack overflow
if (languageCode.equals("zh-rCN"))
locale = Locale.SIMPLIFIED_CHINESE;
if (languageCode.equals("zh-rTW"))
locale = Locale.TRADITIONAL_CHINESE;
Locale.setDefault(locale);
Resources resources = getResources();
Configuration config = resources.getConfiguration();
config.setLocale(locale);
resources.updateConfiguration(config, resources.getDisplayMetrics());
}
}

View File

@ -1,180 +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.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.util.Log;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.View;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import com.squareup.picasso.Picasso;
import net.schueller.peertube.R;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.model.Avatar;
import net.schueller.peertube.model.Me;
import net.schueller.peertube.network.GetUserService;
import net.schueller.peertube.network.RetrofitInstance;
import net.schueller.peertube.network.Session;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import java.util.Objects;
import static net.schueller.peertube.application.AppApplication.getContext;
public class MeActivity extends CommonActivity {
private static final String TAG = "MeActivity";
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater inflater = getMenuInflater();
inflater.inflate(R.menu.menu_top_account, menu);
return true;
}
@Override
public boolean onSupportNavigateUp() {
finish(); // close this activity as oppose to navigating up
return false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_me);
// Attaching the layout to the toolbar object
Toolbar toolbar = findViewById(R.id.tool_bar_me);
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(toolbar);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
LinearLayout account = findViewById(R.id.a_me_account_line);
LinearLayout playlist = findViewById(R.id.a_me_playlist);
LinearLayout settings = findViewById(R.id.a_me_settings);
LinearLayout help = findViewById(R.id.a_me_helpnfeedback);
TextView logout = findViewById(R.id.a_me_logout);
playlist.setOnClickListener(view -> {
Intent playlistActivity = new Intent(getContext(), PlaylistActivity.class);
startActivity(playlistActivity);
});
settings.setOnClickListener(view -> {
Intent settingsActivity = new Intent(getContext(), SettingsActivity.class);
//overridePendingTransition(R.anim.slide_in_bottom, 0);
startActivity(settingsActivity);
});
help.setOnClickListener(view -> {
String url = "https://github.com/sschueller/peertube-android/issues";
Intent i = new Intent(Intent.ACTION_VIEW);
i.setData(Uri.parse(url));
startActivity(i);
});
logout.setOnClickListener(view -> {
Session.getInstance().invalidate();
account.setVisibility(View.GONE);
});
getUserData();
}
private void getUserData() {
String apiBaseURL = APIUrlHelper.getUrlWithVersion(this);
String baseURL = APIUrlHelper.getUrl(this);
GetUserService service = RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(this)).create(GetUserService.class);
Call<Me> call = service.getMe();
call.enqueue(new Callback<Me>() {
final LinearLayout account = findViewById(R.id.a_me_account_line);
@Override
public void onResponse(@NonNull Call<Me> call, @NonNull Response<Me> response) {
if (response.isSuccessful()) {
Me me = response.body();
Log.d(TAG, response.body().toString());
TextView username = findViewById(R.id.a_me_username);
TextView email = findViewById(R.id.a_me_email);
ImageView avatarView = findViewById(R.id.a_me_avatar);
username.setText(me.getUsername());
email.setText(me.getEmail());
Avatar avatar = me.getAccount().getAvatar();
if (avatar != null) {
String avatarPath = avatar.getPath();
Picasso.get()
.load(baseURL + avatarPath)
.into(avatarView);
}
account.setVisibility(View.VISIBLE);
} else {
account.setVisibility(View.GONE);
}
}
@Override
public void onFailure(@NonNull Call<Me> call, @NonNull Throwable t) {
ErrorHelper.showToastFromCommunicationError(MeActivity.this, t);
account.setVisibility(View.GONE);
}
});
}
@Override
protected void onResume() {
super.onResume();
getUserData();
}
}

View File

@ -1,103 +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.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import androidx.activity.viewModels
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.adapter.MultiViewRecyclerViewHolder
import net.schueller.peertube.adapter.PlaylistAdapter
import net.schueller.peertube.database.Video
import net.schueller.peertube.database.VideoViewModel
import net.schueller.peertube.databinding.ActivityPlaylistBinding
class PlaylistActivity : CommonActivity() {
private val TAG = "PlaylistAct"
private val mVideoViewModel: VideoViewModel by viewModels()
private lateinit var mBinding: ActivityPlaylistBinding
override fun onSupportNavigateUp(): Boolean {
finish() // close this activity as oppose to navigating up
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityPlaylistBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(mBinding.toolBarServerAddressBook)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
}
showServers()
}
private fun onVideoClick(video: Video) {
val intent = Intent(this, VideoPlayActivity::class.java)
intent.putExtra(MultiViewRecyclerViewHolder.EXTRA_VIDEOID, video.videoUUID)
startActivity(intent)
}
private fun showServers() {
val adapter = PlaylistAdapter(mutableListOf(), { onVideoClick(it) }).also {
mBinding.serverListRecyclerview.adapter = it
}
// Delete items on swipe
val helper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
AlertDialog.Builder(this@PlaylistActivity)
.setTitle(getString(R.string.remove_video))
.setMessage(getString(R.string.remove_video_warning_message))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
val position = viewHolder.bindingAdapterPosition
val video = adapter.getVideoAtPosition(position)
// Delete the video
mVideoViewModel.delete(video)
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
})
helper.attachToRecyclerView(mBinding.serverListRecyclerview)
// Update the cached copy of the words in the adapter.
mVideoViewModel.allVideos.observe(this, { videos: List<Video> ->
adapter.setVideos(videos)
})
}
companion object
}

View File

@ -1,200 +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 androidx.annotation.NonNull;
import androidx.appcompat.widget.Toolbar;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import android.os.Bundle;
import android.util.Log;
import android.view.KeyEvent;
import android.view.View;
import android.view.inputmethod.EditorInfo;
import android.widget.EditText;
import android.widget.TextView;
import android.widget.Toast;
import net.schueller.peertube.R;
import net.schueller.peertube.adapter.ServerSearchAdapter;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.helper.ErrorHelper;
import net.schueller.peertube.model.ServerList;
import net.schueller.peertube.network.GetServerListDataService;
import net.schueller.peertube.network.RetrofitInstance;
import java.util.ArrayList;
import java.util.Objects;
public class SearchServerActivity extends CommonActivity {
private ServerSearchAdapter serverAdapter;
private SwipeRefreshLayout swipeRefreshLayout;
private EditText searchTextView;
private final static String TAG = "SearchServerActivity";
private int currentStart = 0;
private final int count = 12;
private String lastSearchtext = "";
private TextView emptyView;
private RecyclerView recyclerView;
private boolean isLoading = false;
@Override
public boolean onSupportNavigateUp() {
finish(); // close this activity as oppose to navigating up
return false;
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_search_server);
// Attaching the layout to the toolbar object
Toolbar toolbar = findViewById(R.id.tool_bar_server_selection);
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(toolbar);
Objects.requireNonNull(getSupportActionBar()).setDisplayHomeAsUpEnabled(true);
getSupportActionBar().setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
loadList();
}
TextView.OnEditorActionListener onSearchTextValidated = ( textView, i, keyEvent ) -> {
if ( keyEvent != null && keyEvent.getKeyCode() == KeyEvent.KEYCODE_ENTER
|| i == EditorInfo.IME_ACTION_GO ) {
loadServers(currentStart, count, textView.getText().toString());
}
return false;
};
private void loadList() {
recyclerView = findViewById(R.id.serverRecyclerView);
swipeRefreshLayout = findViewById(R.id.serversSwipeRefreshLayout);
searchTextView = findViewById(R.id.search_server_input_field );
searchTextView.setOnEditorActionListener( onSearchTextValidated );
emptyView = findViewById(R.id.empty_server_selection_view);
RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(SearchServerActivity.this);
recyclerView.setLayoutManager(layoutManager);
serverAdapter = new ServerSearchAdapter(new ArrayList<>(), this);
recyclerView.setAdapter(serverAdapter);
loadServers(currentStart, count, searchTextView.getText().toString() );
recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) {
super.onScrollStateChanged(recyclerView, newState);
}
@Override
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
if (dy > 0) {
// is at end of list?
if (!recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN)) {
if (!isLoading) {
currentStart = currentStart + count;
loadServers(currentStart, count, searchTextView.getText().toString());
}
}
}
}
});
swipeRefreshLayout.setOnRefreshListener(() -> {
// Refresh items
if (!isLoading) {
currentStart = 0;
loadServers(currentStart, count, searchTextView.getText().toString());
}
});
}
private void loadServers(int start, int count, String searchtext) {
isLoading = true;
GetServerListDataService service = RetrofitInstance.getRetrofitInstance(
APIUrlHelper.getServerIndexUrl(SearchServerActivity.this)
, APIUrlHelper.useInsecureConnection(this)).create(GetServerListDataService.class);
if ( !searchtext.equals( lastSearchtext ) )
{
currentStart = 0;
lastSearchtext = searchtext;
}
Call<ServerList> call;
call = service.getInstancesData(start, count, searchtext);
Log.d("URL Called", call.request().url() + "");
call.enqueue(new Callback<ServerList>() {
@Override
public void onResponse(@NonNull Call<ServerList> call, @NonNull Response<ServerList> response) {
if (currentStart == 0) {
serverAdapter.clearData();
}
if (response.body() != null) {
serverAdapter.setData(response.body().getServerArrayList());
}
// no results show no results message
if (currentStart == 0 && serverAdapter.getItemCount() == 0) {
emptyView.setVisibility(View.VISIBLE);
recyclerView.setVisibility(View.GONE);
} else {
emptyView.setVisibility(View.GONE);
recyclerView.setVisibility(View.VISIBLE);
}
isLoading = false;
swipeRefreshLayout.setRefreshing(false);
}
@Override
public void onFailure(@NonNull Call<ServerList> call, @NonNull Throwable t) {
Log.wtf("err", t.fillInStackTrace());
ErrorHelper.showToastFromCommunicationError( SearchServerActivity.this, t );
isLoading = false;
swipeRefreshLayout.setRefreshing(false);
}
});
}
}

View File

@ -1,168 +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.app.Activity
import android.app.AlertDialog
import android.content.DialogInterface
import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import androidx.activity.viewModels
import androidx.fragment.app.FragmentManager
import androidx.preference.PreferenceManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
import net.schueller.peertube.adapter.ServerListAdapter
import net.schueller.peertube.database.Server
import net.schueller.peertube.database.ServerViewModel
import net.schueller.peertube.databinding.ActivityServerAddressBookBinding
import net.schueller.peertube.fragment.AddServerFragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.Session
import net.schueller.peertube.service.LoginService
import java.util.*
class ServerAddressBookActivity : CommonActivity() {
private val TAG = "ServerAddBookAct"
private val mServerViewModel: ServerViewModel by viewModels()
private var addServerFragment: AddServerFragment? = null
private val fragmentManager: FragmentManager by lazy { supportFragmentManager }
private lateinit var mBinding: ActivityServerAddressBookBinding
override fun onSupportNavigateUp(): Boolean {
finish() // close this activity as oppose to navigating up
return false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
mBinding = ActivityServerAddressBookBinding.inflate(layoutInflater)
setContentView(mBinding.root)
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(mBinding.toolBarServerAddressBook)
supportActionBar?.apply {
setDisplayHomeAsUpEnabled(true)
setHomeAsUpIndicator(R.drawable.ic_baseline_close_24)
}
showServers()
mBinding.addServer.setOnClickListener {
Log.d(TAG, "Click")
val fragmentTransaction = fragmentManager.beginTransaction()
addServerFragment = AddServerFragment().also {
fragmentTransaction.replace(R.id.server_book, it)
fragmentTransaction.commit()
mBinding.addServer.hide()
}
}
}
private fun onServerClick(server: Server) {
val sharedPref = PreferenceManager.getDefaultSharedPreferences(this)
val editor = sharedPref.edit()
val serverUrl = APIUrlHelper.cleanServerUrl(server.serverHost)
editor.putString(getString(R.string.pref_api_base_key), serverUrl)
editor.apply()
// Logout if logged in
val session = Session.getInstance()
if (session.isLoggedIn) {
session.invalidate()
}
// attempt authentication if we have a username
if (server.username.isNullOrBlank().not()) {
LoginService.Authenticate(server.username, server.password)
}
// close this activity
finish()
Toast.makeText(this, getString(R.string.server_selection_set_server, serverUrl), Toast.LENGTH_LONG).show()
}
private fun onEditClick(server: Server) {
val fragmentTransaction = fragmentManager.beginTransaction()
addServerFragment = AddServerFragment.newInstance(server).also {
fragmentTransaction.replace(R.id.server_book, it)
fragmentTransaction.commit()
mBinding.addServer.hide()
}
}
private fun showServers() {
val adapter = ServerListAdapter(mutableListOf(), { onServerClick(it) }, { onEditClick(it) }).also {
mBinding.serverListRecyclerview.adapter = it
}
// Delete items on swipe
val helper = ItemTouchHelper(
object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) {
override fun onMove(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder): Boolean {
return false
}
override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
AlertDialog.Builder(this@ServerAddressBookActivity)
.setTitle(getString(R.string.server_book_del_alert_title))
.setMessage(getString(R.string.server_book_del_alert_msg))
.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.cancel) { _: DialogInterface?, _: Int -> adapter.notifyItemChanged(viewHolder.bindingAdapterPosition) }
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
})
helper.attachToRecyclerView(mBinding.serverListRecyclerview)
// Update the cached copy of the words in the adapter.
mServerViewModel.allServers.observe(this, { servers: List<Server> ->
adapter.setServers(servers)
addServerFragment?.let {
val fragmentTransaction = fragmentManager.beginTransaction()
fragmentTransaction.remove(it)
fragmentTransaction.commit()
mBinding.addServer.show()
}
})
}
companion object {
const val EXTRA_REPLY = "net.schueller.peertube.room.REPLY"
}
}

View File

@ -1,137 +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.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.provider.SearchRecentSuggestions;
import android.util.Log;
import androidx.appcompat.app.ActionBar;
import androidx.appcompat.app.AlertDialog.Builder;
import androidx.appcompat.widget.Toolbar;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
import androidx.preference.SwitchPreference;
import net.schueller.peertube.BuildConfig;
import net.schueller.peertube.R;
import net.schueller.peertube.provider.SearchSuggestionsProvider;
public class SettingsActivity extends CommonActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_settings);
getSupportFragmentManager()
.beginTransaction()
.replace(R.id.settings, new SettingsFragment())
.commit();
// Attaching the layout to the toolbar object
Toolbar toolbar = findViewById(R.id.tool_bar_settings);
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(toolbar);
ActionBar actionBar = getSupportActionBar();
if (actionBar != null) {
actionBar.setDisplayHomeAsUpEnabled(true);
actionBar.setHomeAsUpIndicator(R.drawable.ic_baseline_close_24);
}
}
@Override
public boolean onSupportNavigateUp() {
finish(); // close this activity as oppose to navigating up
return false;
}
public static class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onCreatePreferences(Bundle savedInstanceState, String rootKey) {
setPreferencesFromResource(R.xml.root_preferences, rootKey);
// write Build Time into pref
Preference pref = findPreference("pref_buildtime");
assert pref != null;
pref.setSummary(Long.toString(BuildConfig.BUILD_TIME));
// double check disabling SSL
final SwitchPreference insecure = (SwitchPreference) findPreference("pref_accept_insecure");
if (insecure != null) {
insecure.setOnPreferenceChangeListener((preference, newValue) -> {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getContext());
SharedPreferences.Editor editor = sharedPref.edit();
boolean currentValue = sharedPref.getBoolean("pref_accept_insecure", false);
if (newValue instanceof Boolean && ((Boolean) newValue) != currentValue) {
final boolean enable = (Boolean) newValue;
Log.v("pref", "enable: " + enable);
Log.v("pref", "currentValue: " + currentValue);
if (enable) {
new Builder(preference.getContext())
.setTitle(R.string.pref_insecure_confirm_title)
.setMessage(R.string.pref_insecure_confirm_message)
.setIcon(R.drawable.ic_info_black_24dp)
.setNegativeButton(R.string.pref_insecure_confirm_no, (dialog, whichButton) -> {
// do nothing
})
.setPositiveButton(R.string.pref_insecure_confirm_yes, (dialog, whichButton) -> {
// OK has been pressed => force the new value and update the checkbox display
editor.putBoolean("pref_accept_insecure", true);
editor.apply();
insecure.setChecked(true);
}).create().show();
// by default ignore the pref change, which can only be validated when OK is pressed
return false;
}
}
return true;
});
}
//clear search history buttonish
Preference button = findPreference(getString(R.string.pref_clear_history_key));
button.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() {
@Override
public boolean onPreferenceClick(Preference preference) {
new Builder(preference.getContext())
.setTitle(R.string.clear_search_history)
.setMessage(R.string.clear_search_history_prompt)
.setIcon(R.drawable.ic_info_black_24dp)
.setNegativeButton(R.string.pref_insecure_confirm_no, (dialog, whichButton) -> {
// do nothing
})
.setPositiveButton(R.string.pref_insecure_confirm_yes, (dialog, whichButton) -> {
// OK has been pressed
SearchRecentSuggestions suggestions = new SearchRecentSuggestions(getContext(),
SearchSuggestionsProvider.AUTHORITY,
SearchSuggestionsProvider.MODE);
suggestions.clearHistory();
}).create().show();
return true;
}
});
}
}
}

View File

@ -1,590 +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.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
import android.content.Intent
import android.content.pm.PackageManager
import android.database.Cursor
import android.os.Bundle
import android.provider.SearchRecentSuggestions
import android.util.Log
import android.view.Menu
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
import androidx.core.app.ActivityCompat
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.RecyclerView.OnScrollListener
import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
import com.google.android.material.bottomnavigation.BottomNavigationView
import com.google.android.material.navigation.NavigationBarView
import net.schueller.peertube.R
import net.schueller.peertube.R.id
import net.schueller.peertube.R.layout
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.Overview
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.network.GetUserService
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.network.Session
import net.schueller.peertube.provider.SearchSuggestionsProvider
import net.schueller.peertube.service.VideoPlayerService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
private const val TAG = "_VideoListActivity"
class VideoListActivity : CommonActivity() {
private var mMultiViewAdapter: MultiViewRecycleViewAdapter? = null
private var swipeRefreshLayout: SwipeRefreshLayout? = null
private var currentStart = 0
private var currentPage = 1
private val count = 12
private var sort = "-createdAt"
private var filter: String? = null
private var searchQuery = ""
private var subscriptions = false
private var emptyView: TextView? = null
private var recyclerView: RecyclerView? = null
private var isLoading = false
private var overViewActive = false
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(layout.activity_video_list)
filter = null
createBottomBarNavigation()
// Attaching the layout to the toolbar object
val toolbar = findViewById<Toolbar>(id.tool_bar)
// Setting toolbar as the ActionBar with setSupportActionBar() call
setSupportActionBar(toolbar)
// load Video List
createList()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
val inflater = menuInflater
inflater.inflate(R.menu.menu_top_videolist, menu)
// Set an icon in the ActionBar
menu.findItem(id.action_account).setIcon(R.drawable.ic_user)
menu.findItem(id.action_server_address_book).setIcon(R.drawable.ic_server)
val searchMenuItem = menu.findItem(id.action_search)
searchMenuItem.setIcon(R.drawable.ic_search)
// Get the SearchView and set the searchable configuration
val searchManager = getSystemService(SEARCH_SERVICE) as SearchManager
val searchView = searchMenuItem.actionView as SearchView
// Assumes current activity is the searchable activity
searchView.setSearchableInfo(searchManager.getSearchableInfo(componentName))
searchView.setIconifiedByDefault(false) // Do not iconify the widget; expand it by default
searchView.isQueryRefinementEnabled = true
searchMenuItem.actionView.setOnLongClickListener {
Builder(this@VideoListActivity)
.setTitle(getString(R.string.clear_search_history))
.setMessage(getString(R.string.clear_search_history_prompt))
.setPositiveButton(string.ok) { _, _ ->
val suggestions = SearchRecentSuggestions(
applicationContext,
SearchSuggestionsProvider.AUTHORITY,
SearchSuggestionsProvider.MODE
)
suggestions.clearHistory()
}
.setNegativeButton(string.cancel, null)
.setIcon(drawable.ic_dialog_alert)
.show()
true
}
searchMenuItem.setOnActionExpandListener(object : OnActionExpandListener {
override fun onMenuItemActionExpand(menuItem: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(menuItem: MenuItem): Boolean {
searchQuery = ""
Log.d(TAG, "onMenuItemActionCollapse: ")
loadVideos(0, count, sort, filter)
return true
}
})
// TODO, this doesn't work
searchManager.setOnDismissListener {
searchQuery = ""
Log.d(TAG, "onDismiss: ")
loadVideos(0, count, sort, filter)
}
searchView.setOnSuggestionListener(object : OnSuggestionListener {
override fun onSuggestionClick(position: Int): Boolean {
val suggestion = getSuggestion(position)
searchView.setQuery(suggestion, true)
return true
}
private fun getSuggestion(position: Int): String {
val cursor = searchView.suggestionsAdapter.getItem(
position
) as Cursor
return cursor.getString(
cursor.getColumnIndex(SearchManager.SUGGEST_COLUMN_TEXT_1)
)
}
override fun onSuggestionSelect(position: Int): Boolean {
/* Required to implement */
return true
}
})
return true
}
override fun onDestroy() {
super.onDestroy()
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)
// }
// }
// }
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
// as you specify a parent activity in AndroidManifest.xml.
when (item.itemId) {
id.action_search -> //Toast.makeText(this, "Search Selected", Toast.LENGTH_SHORT).show();
return false
id.action_account -> {
// if (!Session.getInstance().isLoggedIn()) {
//Intent intentLogin = new Intent(this, ServerAddressBookActivity.class);
val intentMe = Intent(this, MeActivity::class.java)
this.startActivity(intentMe)
//overridePendingTransition(R.anim.slide_in_bottom, 0);
// this.startActivity(intentLogin);
// } else {
// Intent intentMe = new Intent(this, MeActivity.class);
// this.startActivity(intentMe);
// }
return false
}
id.action_server_address_book -> {
val addressBookActivityIntent = Intent(this, ServerAddressBookActivity::class.java)
openActivityForResult(addressBookActivityIntent)
return false
}
else -> {
}
}
return super.onOptionsItemSelected(item)
}
private fun createList() {
recyclerView = findViewById(id.recyclerView)
swipeRefreshLayout = findViewById(id.swipeRefreshLayout)
emptyView = findViewById(id.empty_view)
val layoutManager: LayoutManager = LinearLayoutManager(this@VideoListActivity)
recyclerView?.layoutManager = layoutManager
mMultiViewAdapter = MultiViewRecycleViewAdapter()
recyclerView?.adapter = mMultiViewAdapter
// loadVideos(currentStart, count, sort, filter)
overViewActive = true
loadOverview(currentPage)
recyclerView?.addOnScrollListener(object : OnScrollListener() {
override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) {
if (dy > 0) {
// is at end of list?
if (!recyclerView.canScrollVertically(RecyclerView.FOCUS_DOWN)) {
if (!isLoading) {
if (overViewActive) {
currentPage++
loadOverview(currentPage)
} else {
currentStart += count
loadVideos(currentStart, count, sort, filter)
}
}
}
}
}
})
swipeRefreshLayout?.setOnRefreshListener {
// Refresh items
if (!isLoading) {
if (overViewActive) {
currentPage = 1
loadOverview(currentPage)
} else {
currentStart = 0
loadVideos(currentStart, count, sort, filter)
}
}
}
}
private fun loadOverview(page: Int) {
isLoading = true
// We set this to default to null so that on initial start there are videos listed.
val apiBaseURL = APIUrlHelper.getUrlWithVersion(this)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(this)).create(
GetVideoDataService::class.java
)
val call: Call<Overview>? = service.getOverviewVideosData(page)
call?.enqueue(object : Callback<Overview?> {
override fun onResponse(call: Call<Overview?>, response: Response<Overview?>) {
if (page == 1) {
mMultiViewAdapter?.clearData()
}
if (response.body() != null) {
val overview = response.body()
if (overview != null) {
if (overview.categories.isNotEmpty()) {
mMultiViewAdapter?.setCategoryTitle(overview.categories[0].category)
mMultiViewAdapter?.setVideoData(overview.categories[0].videos)
}
if (overview.channels.isNotEmpty()) {
mMultiViewAdapter?.setChannelTitle(overview.channels[0].channel)
mMultiViewAdapter?.setVideoData(overview.channels[0].videos)
}
if (overview.tags.isNotEmpty()) {
mMultiViewAdapter?.setTagTitle(overview.tags[0])
mMultiViewAdapter?.setVideoData(overview.tags[0].videos)
}
}
}
// no results show no results message
if (mMultiViewAdapter?.itemCount == 0) {
emptyView!!.visibility = View.VISIBLE
recyclerView!!.visibility = View.GONE
} else {
emptyView!!.visibility = View.GONE
recyclerView!!.visibility = View.VISIBLE
}
isLoading = false
swipeRefreshLayout!!.isRefreshing = false
}
override fun onFailure(call: Call<Overview?>, t: Throwable) {
Log.wtf("err", t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(this@VideoListActivity, t)
isLoading = false
swipeRefreshLayout!!.isRefreshing = false
}
})
}
private fun loadVideos(start: Int, count: Int, sort: String, filter: String?) {
isLoading = true
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val nsfw = if (sharedPref.getBoolean(getString(R.string.pref_show_nsfw_key), false)) "both" else "false"
//
// Locale locale = getResources().getConfiguration().locale;
// String country = locale.getLanguage();
//
// HashSet<String> countries = new HashSet<>(1);
// countries.add(country);
// We set this to default to null so that on initial start there are videos listed.
val languages = sharedPref.getStringSet(getString(R.string.pref_video_language_key), null)
val apiBaseURL = APIUrlHelper.getUrlWithVersion(this)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(this)).create(
GetVideoDataService::class.java
)
val call: Call<VideoList> = when {
searchQuery != "" -> {
service.searchVideosData(start, count, sort, nsfw, searchQuery, filter, languages)
}
subscriptions -> {
val userService =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(this)).create(
GetUserService::class.java
)
userService.getVideosSubscripions(start, count, sort)
}
else -> {
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 (currentStart == 0) {
mMultiViewAdapter!!.clearData()
}
if (response.body() != null) {
val videoList = response.body()
if (videoList != null) {
mMultiViewAdapter!!.setVideoListData(videoList)
}
}
// no results show no results message
if (currentStart == 0 && mMultiViewAdapter!!.itemCount == 0) {
emptyView!!.visibility = View.VISIBLE
recyclerView!!.visibility = View.GONE
} else {
emptyView!!.visibility = View.GONE
recyclerView!!.visibility = View.VISIBLE
}
isLoading = false
swipeRefreshLayout!!.isRefreshing = false
}
override fun onFailure(call: Call<VideoList?>, t: Throwable) {
Log.wtf("err", t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(this@VideoListActivity, t)
isLoading = false
swipeRefreshLayout!!.isRefreshing = false
}
})
}
override fun onResume() {
super.onResume()
// only check when we actually need the permission
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
if (ActivityCompat.checkSelfPermission(
this,
permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED &&
sharedPref.getBoolean(getString(R.string.pref_torrent_player_key), false)
) {
ActivityCompat.requestPermissions(this, arrayOf(permission.WRITE_EXTERNAL_STORAGE), 0)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
handleIntent(intent)
}
private fun handleIntent(intent: Intent) {
if (Intent.ACTION_SEARCH == intent.action) {
val query = intent.getStringExtra(SearchManager.QUERY)
val suggestions = SearchRecentSuggestions(
this,
SearchSuggestionsProvider.AUTHORITY,
SearchSuggestionsProvider.MODE
)
// Save recent searches
suggestions.saveRecentQuery(query, null)
if (query != null) {
searchQuery = query
}
loadVideos(0, count, sort, filter)
}
}
override fun onSearchRequested(): Boolean {
val appData = Bundle()
startSearch(null, false, appData, false)
return true
}
private fun createBottomBarNavigation() {
// Get Bottom Navigation
val navigation = findViewById<BottomNavigationView>(id.navigation)
// Always show text label
navigation.labelVisibilityMode = NavigationBarView.LABEL_VISIBILITY_LABELED
// Add Icon font
val navMenu = navigation.menu
navMenu.findItem(id.navigation_overview).setIcon(R.drawable.ic_globe)
navMenu.findItem(id.navigation_trending).setIcon(R.drawable.ic_trending)
navMenu.findItem(id.navigation_recent).setIcon(R.drawable.ic_plus_circle)
navMenu.findItem(id.navigation_local).setIcon(R.drawable.ic_local)
navMenu.findItem(id.navigation_subscriptions).setIcon(R.drawable.ic_subscriptions)
// navMenu.findItem(R.id.navigation_account).setIcon(
// new IconicsDrawable(this, FontAwesome.Icon.faw_user_circle));
// Click Listener
navigation.setOnItemSelectedListener { menuItem: MenuItem ->
when (menuItem.itemId) {
id.navigation_overview -> {
// TODO
if (!isLoading) {
currentPage = 1
loadOverview(currentPage)
overViewActive = true
}
return@setOnItemSelectedListener true
}
id.navigation_trending -> {
//Log.v(TAG, "navigation_trending");
if (!isLoading) {
overViewActive = false
sort = "-trending"
currentStart = 0
filter = null
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnItemSelectedListener true
}
id.navigation_recent -> {
if (!isLoading) {
overViewActive = false
sort = "-createdAt"
currentStart = 0
filter = null
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
return@setOnItemSelectedListener true
}
id.navigation_local -> {
//Log.v(TAG, "navigation_trending");
if (!isLoading) {
overViewActive = false
sort = "-publishedAt"
filter = "local"
currentStart = 0
subscriptions = false
loadVideos(currentStart, count, sort, filter)
}
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)
openActivityForResult(addressBookActivityIntent)
return@setOnItemSelectedListener false
} else {
if (!isLoading) {
overViewActive = false
sort = "-publishedAt"
filter = null
currentStart = 0
subscriptions = true
loadVideos(currentStart, count, sort, filter)
}
return@setOnItemSelectedListener true
}
}
false
}
// TODO: on double click jump to top and reload
// navigation.setOnNavigationItemReselectedListener(menuItemReselected -> {
// switch (menuItemReselected.getItemId()) {
// case R.id.navigation_home:
// if (!isLoading) {
// sort = "-createdAt";
// currentStart = 0;
// filter = null;
// subscriptions = false;
// loadVideos(currentStart, count, sort, filter);
// }
// case R.id.navigation_trending:
// if (!isLoading) {
// sort = "-trending";
// currentStart = 0;
// filter = null;
// subscriptions = false;
// loadVideos(currentStart, count, sort, filter);
// }
// case R.id.navigation_local:
// if (!isLoading) {
// sort = "-publishedAt";
// filter = "local";
// currentStart = 0;
// subscriptions = false;
// loadVideos(currentStart, count, sort, filter);
// }
// case R.id.navigation_subscriptions:
// if (Session.getInstance().isLoggedIn()) {
// if (!isLoading) {
// sort = "-publishedAt";
// filter = null;
// currentStart = 0;
// subscriptions = true;
// loadVideos(currentStart, count, sort, filter);
// }
// }
// }
// });
}
companion object {
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
}
}

View File

@ -1,461 +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 androidx.appcompat.app.AppCompatActivity
import android.annotation.SuppressLint
import net.schueller.peertube.fragment.VideoPlayerFragment
import net.schueller.peertube.R
import android.app.RemoteAction
import android.app.PendingIntent
import com.google.android.exoplayer2.ui.PlayerNotificationManager
import android.app.PictureInPictureParams
import android.content.*
import android.content.res.Configuration
import android.graphics.drawable.Icon
import android.os.Bundle
import android.text.TextUtils
import net.schueller.peertube.fragment.VideoMetaDataFragment
import android.widget.RelativeLayout
import android.widget.FrameLayout
import android.util.TypedValue
import android.view.WindowManager
import net.schueller.peertube.service.VideoPlayerService
import net.schueller.peertube.helper.VideoHelper
import androidx.annotation.RequiresApi
import android.os.Build
import android.util.Log
import android.util.Rational
import androidx.fragment.app.Fragment
import net.schueller.peertube.fragment.VideoDescriptionFragment
import java.util.ArrayList
class VideoPlayActivity : CommonActivity() {
private var receiver: BroadcastReceiver? = null
//This can only be called when in entering pip mode which can't happen if the device doesn't support pip mode.
@SuppressLint("NewApi")
fun makePipControls() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val actions = ArrayList<RemoteAction>()
var actionIntent = Intent(getString(R.string.app_background_audio))
var pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
@SuppressLint("NewApi", "LocalSuppress") var icon = Icon.createWithResource(
applicationContext, android.R.drawable.stat_sys_speakerphone
)
@SuppressLint("NewApi", "LocalSuppress") var remoteAction =
RemoteAction(icon!!, "close pip", "from pip window custom command", pendingIntent!!)
actions.add(remoteAction)
actionIntent = Intent(PlayerNotificationManager.ACTION_STOP)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_stop
)
remoteAction = RemoteAction(icon, "play", "stop the media", pendingIntent)
actions.add(remoteAction)
assert(videoPlayerFragment != null)
if (videoPlayerFragment!!.isPaused) {
Log.e(TAG, "setting actions with play button")
actionIntent = Intent(PlayerNotificationManager.ACTION_PLAY)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_play
)
remoteAction = RemoteAction(icon, "play", "play the media", pendingIntent)
} else {
Log.e(TAG, "setting actions with pause button")
actionIntent = Intent(PlayerNotificationManager.ACTION_PAUSE)
pendingIntent =
PendingIntent.getBroadcast(applicationContext, REQUEST_CODE, actionIntent, 0)
icon = Icon.createWithResource(
applicationContext,
com.google.android.exoplayer2.ui.R.drawable.exo_notification_pause
)
remoteAction = RemoteAction(icon, "pause", "pause the media", pendingIntent)
}
actions.add(remoteAction)
//add custom actions to pip window
val params = PictureInPictureParams.Builder()
.setActions(actions)
.build()
setPictureInPictureParams(params)
}
private fun changedToPipMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.showControls(false)
//create custom actions
makePipControls()
//setup receiver to handle customer actions
val filter = IntentFilter()
filter.addAction(PlayerNotificationManager.ACTION_STOP)
filter.addAction(PlayerNotificationManager.ACTION_PAUSE)
filter.addAction(PlayerNotificationManager.ACTION_PLAY)
filter.addAction(getString(R.string.app_background_audio))
receiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val action = intent.action!!
if (action == PlayerNotificationManager.ACTION_PAUSE) {
videoPlayerFragment.pauseVideo()
makePipControls()
}
if (action == PlayerNotificationManager.ACTION_PLAY) {
videoPlayerFragment.unPauseVideo()
makePipControls()
}
if (action == getString(R.string.app_background_audio)) {
unregisterReceiver(receiver)
finish()
}
if (action == PlayerNotificationManager.ACTION_STOP) {
unregisterReceiver(receiver)
finishAndRemoveTask()
}
}
}
registerReceiver(receiver, filter)
Log.v(TAG, "switched to pip ")
floatMode = true
videoPlayerFragment.showControls(false)
}
private fun changedToNormalMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
(fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.showControls(true)
if (receiver != null) {
unregisterReceiver(receiver)
}
Log.v(TAG, "switched to normal")
floatMode = false
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Set theme
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
setTheme(
resources.getIdentifier(
sharedPref.getString(
getString(R.string.pref_theme_key),
getString(R.string.app_default_theme)
),
"style",
packageName
)
)
setContentView(R.layout.activity_video_play)
// get video ID
val intent = intent
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
val playingVideo = videoPlayerFragment.videoUuid
Log.v(TAG, "oncreate click: $videoUuid is trying to replace: $playingVideo")
when {
TextUtils.isEmpty(playingVideo) -> {
Log.v(TAG, "oncreate no video currently playing")
videoPlayerFragment.start(videoUuid)
}
playingVideo != videoUuid -> {
Log.v(TAG, "oncreate different video playing currently")
videoPlayerFragment.stopVideo()
videoPlayerFragment.start(videoUuid)
}
else -> {
Log.v(TAG, "oncreate same video playing currently")
}
}
// if we are in landscape set the video to fullscreen
val orientation = this.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
setIntent(intent)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
val videoUuid = intent.getStringExtra(VideoListActivity.EXTRA_VIDEOID)
Log.v(
TAG,
"new intent click: " + videoUuid + " is trying to replace: " + videoPlayerFragment.videoUuid
)
val playingVideo = videoPlayerFragment.videoUuid
when {
TextUtils.isEmpty(playingVideo) -> {
Log.v(TAG, "new intent no video currently playing")
videoPlayerFragment.start(videoUuid)
}
playingVideo != videoUuid -> {
Log.v(TAG, "new intent different video playing currently")
videoPlayerFragment.stopVideo()
videoPlayerFragment.start(videoUuid)
}
else -> {
Log.v(TAG, "new intent same video playing currently")
}
}
// if we are in landscape set the video to fullscreen
val orientation = this.resources.configuration.orientation
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
Log.v(TAG, "onConfigurationChanged()...")
super.onConfigurationChanged(newConfig)
// Checking the orientation changes of the screen
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
setOrientation(true)
} else if (newConfig.orientation == Configuration.ORIENTATION_PORTRAIT) {
setOrientation(false)
}
}
private fun setOrientation(isLandscape: Boolean) {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val videoMetaFragment =
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
assert(videoPlayerFragment != null)
val params = videoPlayerFragment!!.requireView().layoutParams as RelativeLayout.LayoutParams
params.width = FrameLayout.LayoutParams.MATCH_PARENT
params.height =
if (isLandscape) FrameLayout.LayoutParams.MATCH_PARENT else TypedValue.applyDimension(
TypedValue.COMPLEX_UNIT_DIP,
250f,
resources.displayMetrics
)
.toInt()
videoPlayerFragment.requireView().layoutParams = params
if (videoMetaFragment != null) {
val transaction = fragmentManager.beginTransaction()
.setCustomAnimations(android.R.anim.fade_in, android.R.anim.fade_out)
if (isLandscape) {
transaction.hide(videoMetaFragment)
} else {
transaction.show(videoMetaFragment)
}
transaction.commit()
}
videoPlayerFragment.setIsFullscreen(isLandscape)
if (isLandscape) {
window.addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
} else {
window.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN)
}
}
override fun onDestroy() {
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.destroyVideo()
super.onDestroy()
Log.v(TAG, "onDestroy...")
}
override fun onPause() {
super.onPause()
Log.v(TAG, "onPause()...")
}
override fun onResume() {
super.onResume()
Log.v(TAG, "onResume()...")
}
override fun onStop() {
super.onStop()
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
videoPlayerFragment.stopVideo()
// TODO: doesn't remove fragment??
val fragment: Fragment? = supportFragmentManager.findFragmentByTag(VideoDescriptionFragment.TAG)
if (fragment != null) {
Log.v(TAG, "remove VideoDescriptionFragment")
supportFragmentManager.beginTransaction().remove(fragment).commit()
}
Log.v(TAG, "onStop()...")
}
override fun onStart() {
super.onStart()
Log.v(TAG, "onStart()...")
}
@SuppressLint("NewApi")
public override fun onUserLeaveHint() {
Log.v(TAG, "onUserLeaveHint()...")
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
val videoMetaDataFragment =
fragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?
val backgroundBehavior = sharedPref.getString(
getString(R.string.pref_background_behavior_key),
getString(R.string.pref_background_stop_key)
)
assert(videoPlayerFragment != null)
assert(backgroundBehavior != null)
if (videoMetaDataFragment!!.isLeaveAppExpected) {
super.onUserLeaveHint()
return
}
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
Log.v(TAG, "stop the video")
videoPlayerFragment!!.pauseVideo()
stopService(Intent(this, VideoPlayerService::class.java))
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
Log.v(TAG, "play the Audio")
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
Log.v(TAG, "play in floating video")
//canEnterPIPMode makes sure API level is high enough
if (VideoHelper.canEnterPipMode(this)) {
Log.v(TAG, "enabling pip")
enterPipMode()
} else {
Log.v(TAG, "unable to use pip")
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback")
super.onBackPressed()
}
}
// @RequiresApi(api = Build.VERSION_CODES.O)
@SuppressLint("NewApi")
override fun onBackPressed() {
Log.v(TAG, "onBackPressed()...")
val sharedPref = getSharedPreferences(packageName + "_preferences", Context.MODE_PRIVATE)
val videoPlayerFragment =
(supportFragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?)!!
// copying Youtube behavior to have back button exit full screen.
if (videoPlayerFragment.getIsFullscreen()) {
Log.v(TAG, "exiting full screen")
videoPlayerFragment.fullScreenToggle()
return
}
// pause video if pref is enabled
if (sharedPref.getBoolean(getString(R.string.pref_back_pause_key), true)) {
videoPlayerFragment.pauseVideo()
}
val backgroundBehavior = sharedPref.getString(
getString(R.string.pref_background_behavior_key),
getString(R.string.pref_background_stop_key)
)!!
if (backgroundBehavior == getString(R.string.pref_background_stop_key)) {
Log.v(TAG, "stop the video")
videoPlayerFragment.pauseVideo()
stopService(Intent(this, VideoPlayerService::class.java))
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_audio_key)) {
Log.v(TAG, "play the Audio")
super.onBackPressed()
} else if (backgroundBehavior == getString(R.string.pref_background_float_key)) {
Log.v(TAG, "play in floating video")
//canEnterPIPMode makes sure API level is high enough
if (VideoHelper.canEnterPipMode(this)) {
Log.v(TAG, "enabling pip")
enterPipMode()
//fixes problem where back press doesn't bring up video list after returning from PIP mode
val intentSettings = Intent(this, VideoListActivity::class.java)
this.startActivity(intentSettings)
} else {
Log.v(TAG, "Unable to enter PIP mode")
super.onBackPressed()
}
} else {
// Deal with bad entries from older version
Log.v(TAG, "No setting, fallback")
super.onBackPressed()
}
}
@RequiresApi(api = Build.VERSION_CODES.O)
fun enterPipMode() {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
if (videoPlayerFragment!!.videoAspectRatio == 0.toFloat()) {
Log.i(TAG, "impossible to switch to pip")
} else {
val rational = Rational((videoPlayerFragment.videoAspectRatio * 100).toInt(), 100)
val mParams = PictureInPictureParams.Builder()
.setAspectRatio(rational) // .setSourceRectHint(new Rect(0,500,400,600))
.build()
enterPictureInPictureMode(mParams)
}
}
override fun onPictureInPictureModeChanged(
isInPictureInPictureMode: Boolean,
newConfig: Configuration
) {
val fragmentManager = supportFragmentManager
val videoPlayerFragment =
fragmentManager.findFragmentById(R.id.video_player_fragment) as VideoPlayerFragment?
if (videoPlayerFragment != null) {
if (isInPictureInPictureMode) {
changedToPipMode()
Log.v(TAG, "switched to pip ")
videoPlayerFragment.useController(false)
} else {
changedToNormalMode()
Log.v(TAG, "switched to normal")
videoPlayerFragment.useController(true)
}
} else {
Log.e(TAG, "videoPlayerFragment is NULL")
}
}
companion object {
private const val TAG = "VideoPlayActivity"
var floatMode = false
private const val REQUEST_CODE = 101
}
}

View File

@ -1,175 +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.adapter;
import android.content.Context;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import com.mikepenz.iconics.Iconics;
import com.squareup.picasso.Picasso;
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;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.appcompat.widget.PopupMenu;
import androidx.recyclerview.widget.RecyclerView;
import static net.schueller.peertube.activity.VideoListActivity.EXTRA_VIDEOID;
public class ChannelAdapter extends RecyclerView.Adapter<ChannelAdapter.AccountViewHolder> {
private ArrayList<Video> videoList;
private Context context;
private String baseUrl;
public ChannelAdapter(ArrayList<Video> videoList, Context context) {
this.videoList = videoList;
this.context = context;
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view = layoutInflater.inflate(R.layout.row_account_video, parent, false);
baseUrl = APIUrlHelper.getUrl(context);
return new AccountViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull AccountViewHolder holder, int position) {
Picasso.get()
.load(baseUrl + videoList.get(position).getPreviewPath())
.into(holder.thumb);
Avatar avatar = videoList.get(position).getAccount().getAvatar();
if (avatar != null) {
String avatarPath = avatar.getPath();
Picasso.get()
.load(baseUrl + avatarPath)
.into(holder.avatar);
}
// set Name
holder.name.setText(videoList.get(position).getName());
// set duration
// TODO
// holder.videoDuration.setText( MetaDataHelper.getDuration(videoList.get(position).getDuration().toLong()));
// set age and view count
holder.videoMeta.setText(
MetaDataHelper.getMetaString(videoList.get(position).getCreatedAt(),
videoList.get(position).getViews(),
context,
false
)
);
// set owner
holder.videoOwner.setText(
MetaDataHelper.getOwnerString(videoList.get(position).getAccount(),
context, true
)
);
holder.mView.setOnClickListener(v -> {
Intent intent = new Intent(context,VideoPlayActivity.class);
intent.putExtra(EXTRA_VIDEOID, videoList.get(position).getUuid());
context.startActivity(intent);
});
holder.moreButton.setText(R.string.video_more_icon);
new Iconics.Builder().on(holder.moreButton).build();
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) {
videoList.addAll(data);
this.notifyDataSetChanged();
}
public void clearData() {
videoList.clear();
this.notifyDataSetChanged();
}
@Override
public int getItemCount() {
return videoList.size();
}
class AccountViewHolder extends RecyclerView.ViewHolder {
TextView name, videoMeta, videoOwner, moreButton, videoDuration;
ImageView thumb, avatar;
View mView;
AccountViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.sl_row_name);
thumb = itemView.findViewById(R.id.thumb);
avatar = itemView.findViewById(R.id.avatar);
videoMeta = itemView.findViewById(R.id.videoMeta);
videoOwner = itemView.findViewById(R.id.videoOwner);
moreButton = itemView.findViewById(R.id.moreButton);
videoDuration = itemView.findViewById(R.id.video_duration);
mView = itemView;
}
}
}

View File

@ -1,134 +0,0 @@
package net.schueller.peertube.adapter;
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.R
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(private val videoMetaDataFragment: VideoMetaDataFragment? = null) : RecyclerView.Adapter<MultiViewRecyclerViewHolder>() {
private var items = ArrayList<OverviewRecycleViewItem>()
set(value) {
field = value
notifyDataSetChanged()
}
fun setVideoListData(videoList: VideoList) {
items.addAll(videoList.videos)
notifyDataSetChanged()
}
fun setVideoData(videos: ArrayList<Video>) {
items.addAll(videos)
notifyDataSetChanged()
}
fun setVideoMeta(videoMetaViewItem: VideoMetaViewItem) {
items.add(videoMetaViewItem)
notifyDataSetChanged()
}
fun setCategoryTitle(category: Category) {
items.add(category)
notifyDataSetChanged()
}
fun setChannelTitle(channel: Channel) {
items.add(channel)
notifyDataSetChanged()
}
fun setTagTitle(tag: TagVideo) {
items.add(tag)
notifyDataSetChanged()
}
fun setVideoComment(commentThread: CommentThread) {
items.add(commentThread)
notifyDataSetChanged()
}
fun clearData() {
items.clear()
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MultiViewRecyclerViewHolder {
return when(viewType){
R.layout.item_category_title -> MultiViewRecyclerViewHolder.CategoryViewHolder(
ItemCategoryTitleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
R.layout.item_channel_title -> MultiViewRecyclerViewHolder.ChannelViewHolder(
ItemChannelTitleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
R.layout.item_tag_title -> MultiViewRecyclerViewHolder.TagViewHolder(
ItemTagTitleBinding.inflate(
LayoutInflater.from(parent.context),
parent,
false
)
)
R.layout.row_video_list -> MultiViewRecyclerViewHolder.VideoViewHolder(
RowVideoListBinding.inflate(
LayoutInflater.from(parent.context),
parent,
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")
}
}
override fun onBindViewHolder(holder: MultiViewRecyclerViewHolder, position: Int) {
when(holder){
is MultiViewRecyclerViewHolder.VideoViewHolder -> holder.bind(items[position] as Video)
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)
}
}
override fun getItemCount() = items.size
override fun getItemViewType(position: Int): Int {
return when(items[position]){
is Video -> R.layout.row_video_list
is Channel -> R.layout.item_channel_title
is Category -> R.layout.item_category_title
is TagVideo -> R.layout.item_tag_title
is VideoMetaViewItem -> R.layout.item_video_meta
is CommentThread -> R.layout.item_video_comments_overview
else -> { return 0}
}
}
}

View File

@ -1,562 +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.adapter
import android.app.AlertDialog
import android.content.Context
import android.content.DialogInterface
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.mikepenz.iconics.Iconics.Builder
import com.squareup.picasso.Picasso
import net.schueller.peertube.R
import net.schueller.peertube.R.*
import net.schueller.peertube.activity.AccountActivity
import net.schueller.peertube.activity.VideoListActivity
import net.schueller.peertube.activity.VideoPlayActivity
import net.schueller.peertube.databinding.*
import net.schueller.peertube.fragment.VideoMetaDataFragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.MetaDataHelper.getCreatorAvatar
import net.schueller.peertube.helper.MetaDataHelper.getCreatorString
import net.schueller.peertube.helper.MetaDataHelper.getDuration
import net.schueller.peertube.helper.MetaDataHelper.getMetaString
import net.schueller.peertube.helper.MetaDataHelper.getOwnerString
import net.schueller.peertube.helper.MetaDataHelper.isChannel
import net.schueller.peertube.intents.Intents
import net.schueller.peertube.model.*
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetUserService
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.network.Session
import okhttp3.MediaType.Companion.toMediaType
import okhttp3.RequestBody.Companion.toRequestBody
import okhttp3.ResponseBody
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
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)
}
// Add to playlist
binding.videoAddToPlaylistWrapper.setOnClickListener {
videoMetaDataFragment.saveToPlaylist(video)
Toast.makeText(context, context.getString(string.saved_to_playlist), Toast.LENGTH_SHORT).show()
}
binding.videoBlockWrapper.setOnClickListener {
Toast.makeText(
context,
context.getString(string.video_feature_not_yet_implemented),
Toast.LENGTH_SHORT
).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
}
// created at / views
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context,
true
)
// owner / creator
val displayNameAndHost = getOwnerString(video.account, context)
if (isChannel(video)) {
binding.videoBy.text = context.resources.getString(string.video_by_line, displayNameAndHost)
} else {
binding.videoBy.visibility = GONE
}
binding.videoOwner.text = getCreatorString(video, context)
// owner / creator Avatar
val avatar = getCreatorAvatar(video, context)
if (avatar != null) {
val baseUrl = APIUrlHelper.getUrl(context)
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// videoOwnerSubscribers
binding.videoOwnerSubscribers.text = context.resources.getQuantityString(R.plurals.video_channel_subscribers, video.channel.followersCount, video.channel.followersCount)
// video owner click
binding.videoCreatorInfo.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// avatar click
binding.avatar.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(Companion.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// 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.
}
})
}
// TODO: update subscriber count
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 {
AlertDialog.Builder(context)
.setTitle(context.getString(string.video_sub_del_alert_title))
.setMessage(context.getString(string.video_sub_del_alert_msg))
.setPositiveButton(android.R.string.ok) { _: DialogInterface?, _: Int ->
// Yes
val payload = video.channel.name + "@" + video.channel.host
val call = userService.unsubscribe(payload)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
if (response.isSuccessful) {
binding.videoOwnerSubscribeButton.setText(string.subscribe)
isSubscribed = false
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
// Do nothing.
}
})
}
.setNegativeButton(android.R.string.cancel) { _: DialogInterface?, _: Int ->
// No
}
.setIcon(android.R.drawable.ic_dialog_alert)
.show()
}
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
}
}
}
class ChannelViewHolder(private val binding: ItemChannelTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(channel: Channel) {
val context = binding.avatar.context
val baseUrl = APIUrlHelper.getUrl(context)
// Avatar
val avatar: Avatar? = channel.avatar
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.placeholder(R.drawable.test_image)
.into(binding.avatar)
}
binding.textViewTitle.text = channel.displayName
}
}
class TagViewHolder(private val binding: ItemTagTitleBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(tag: TagVideo) {
binding.textViewTitle.text = tag.tag
}
}
class VideoViewHolder(private val binding: RowVideoListBinding) : MultiViewRecyclerViewHolder(binding) {
fun bind(video: Video) {
val context = binding.thumb.context
val baseUrl = APIUrlHelper.getUrl(context)
// Temp Loading Image
Picasso.get()
.load(baseUrl + video.previewPath)
.placeholder(R.drawable.test_image)
.error(R.drawable.test_image)
.into(binding.thumb)
// Avatar
val avatar = getCreatorAvatar(video, context)
if (avatar != null) {
val avatarPath = avatar.path
Picasso.get()
.load(baseUrl + avatarPath)
.into(binding.avatar)
}
// set Name
binding.slRowName.text = video.name
// set duration (if not live stream)
if (video.live) {
binding.videoDuration.setText(string.video_list_live_marker)
binding.videoDuration.setBackgroundColor(ContextCompat.getColor(context, color.durationLiveBackgroundColor))
} else {
binding.videoDuration.text = getDuration(video.duration.toLong())
binding.videoDuration.setBackgroundColor(ContextCompat.getColor(context, color.durationBackgroundColor))
}
// set age and view count
binding.videoMeta.text = getMetaString(
video.createdAt,
video.views,
context
)
// set owner
val displayNameAndHost = getOwnerString(video.account, context, true)
binding.videoOwner.text = getCreatorString(video, context, true)
// video owner click
binding.videoOwner.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(VideoListActivity.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// avatar click
binding.avatar.setOnClickListener {
val intent = Intent(context, AccountActivity::class.java)
intent.putExtra(Companion.EXTRA_ACCOUNTDISPLAYNAME, displayNameAndHost)
context.startActivity(intent)
}
// Video Click
binding.root.setOnClickListener {
val intent = Intent(context, VideoPlayActivity::class.java)
intent.putExtra(Companion.EXTRA_VIDEOID, video.uuid)
context.startActivity(intent)
}
// More Button
binding.moreButton.setText(string.video_more_icon)
Builder().on(binding.moreButton).build()
binding.moreButton.setOnClickListener { v: View? ->
val popup = PopupMenu(
context,
v!!
)
popup.setOnMenuItemClickListener { menuItem: MenuItem ->
when (menuItem.itemId) {
id.menu_share -> {
Intents.Share(context, video)
return@setOnMenuItemClickListener true
}
else -> return@setOnMenuItemClickListener false
}
}
popup.inflate(menu.menu_video_row_mode)
popup.show()
}
}
}
fun updateVideoRating(video: Video?, binding: ItemVideoMetaBinding) {
when (videoRating!!.rating) {
RATING_NONE -> {
Log.v("MWCVH", "RATING_NONE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_LIKE -> {
Log.v("MWCVH", "RATING_LIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up_filled)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down)
}
RATING_DISLIKE -> {
Log.v("MWCVH", "RATING_DISLIKE")
binding.videoThumbsUp.setImageResource(R.drawable.ic_thumbs_up)
binding.videoThumbsDown.setImageResource(R.drawable.ic_thumbs_down_filled)
}
}
// Update the texts
binding.videoThumbsUpTotal.text = video?.likes.toString()
binding.videoThumbsDownTotal.text = video?.dislikes.toString()
}
/**
* TODO: move this out and get update when rating changes
*/
fun rateVideo(like: Boolean, video: Video, context: Context, binding: ItemVideoMetaBinding) {
if (Session.getInstance().isLoggedIn) {
val ratePayload: String = when (videoRating!!.rating) {
RATING_LIKE -> if (like) RATING_NONE else RATING_DISLIKE
RATING_DISLIKE -> if (like) RATING_LIKE else RATING_NONE
RATING_NONE -> if (like) RATING_LIKE else RATING_DISLIKE
else -> if (like) RATING_LIKE else RATING_DISLIKE
}
val body = "{\"rating\":\"$ratePayload\"}".toRequestBody("application/json".toMediaType())
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL, APIUrlHelper.useInsecureConnection(
context
)
).create(
GetVideoDataService::class.java
)
val call = videoDataService.rateVideo(video.id, body)
call.enqueue(object : Callback<ResponseBody?> {
override fun onResponse(
call: Call<ResponseBody?>,
response: Response<ResponseBody?>
) {
// if 20x, update likes/dislikes
if (response.isSuccessful) {
val previousRating = videoRating!!.rating
// Update the likes/dislikes count of the video, if needed.
// This is only a visual trick, as the actual like/dislike count has
// already been modified on the PeerTube instance.
if (previousRating != ratePayload) {
when (previousRating) {
RATING_NONE -> if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
} else {
video.dislikes = video.dislikes + 1
}
RATING_LIKE -> {
video.likes = video.likes - 1
if (ratePayload == RATING_DISLIKE) {
video.dislikes = video.dislikes + 1
}
}
RATING_DISLIKE -> {
video.dislikes = video.dislikes - 1
if (ratePayload == RATING_LIKE) {
video.likes = video.likes + 1
}
}
}
}
videoRating!!.rating = ratePayload
updateVideoRating(video, binding)
}
}
override fun onFailure(call: Call<ResponseBody?>, t: Throwable) {
Toast.makeText(
context,
context.getString(string.video_rating_failed),
Toast.LENGTH_SHORT
).show()
}
})
} else {
Toast.makeText(
context,
context.getString(string.video_login_required_for_service),
Toast.LENGTH_SHORT
).show()
}
}
companion object {
private const val RATING_NONE = "none"
private const val RATING_LIKE = "like"
private const val RATING_DISLIKE = "dislike"
const val EXTRA_VIDEOID = "VIDEOID"
const val EXTRA_ACCOUNTDISPLAYNAME = "ACCOUNTDISPLAYNAMEANDHOST"
}
}

View File

@ -1,63 +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.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.database.Video
import net.schueller.peertube.databinding.RowPlaylistBinding
class PlaylistAdapter(private val mVideos: MutableList<Video>, private val onClick: (Video) -> Unit) : RecyclerView.Adapter<PlaylistAdapter.VideoViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VideoViewHolder {
val binding = RowPlaylistBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return VideoViewHolder(binding)
}
override fun onBindViewHolder(holder: VideoViewHolder, position: Int) {
holder.bind(mVideos[position])
}
fun setVideos(videos: List<Video>) {
mVideos.clear()
mVideos.addAll(videos)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return mVideos.size
}
inner class VideoViewHolder(private val binding: RowPlaylistBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(video: Video) {
binding.videoName.text = video.videoName
binding.videoDescription.text = video.videoDescription
binding.root.setOnClickListener { onClick(video) }
}
}
fun getVideoAtPosition(position: Int): Video {
return mVideos[position]
}
}

View File

@ -1,67 +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.adapter
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import net.schueller.peertube.adapter.ServerListAdapter.ServerViewHolder
import net.schueller.peertube.database.Server
import net.schueller.peertube.databinding.RowServerAddressBookBinding
import net.schueller.peertube.utils.visibleIf
class ServerListAdapter(private val mServers: MutableList<Server>, private val onClick: (Server) -> Unit, private val onEditClick: (Server) -> Unit) : RecyclerView.Adapter<ServerViewHolder>() {
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ServerViewHolder {
val binding = RowServerAddressBookBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return ServerViewHolder(binding)
}
override fun onBindViewHolder(holder: ServerViewHolder, position: Int) {
holder.bind(mServers[position])
}
fun setServers(servers: List<Server>) {
mServers.clear()
mServers.addAll(servers)
notifyDataSetChanged()
}
override fun getItemCount(): Int {
return mServers.size
}
inner class ServerViewHolder (private val binding: RowServerAddressBookBinding) : RecyclerView.ViewHolder(binding.root) {
fun bind(server: Server) {
binding.serverLabelRow.text = server.serverName
binding.serverUrlRow.text = server.serverHost
binding.sbRowHasLoginIcon.visibleIf { server.username.isNullOrBlank().not() }
binding.root.setOnClickListener { onClick(server) }
binding.editIcon.setOnClickListener { onEditClick(server) }
}
}
fun getServerAtPosition(position: Int): Server {
return mServers[position]
}
}

View File

@ -1,175 +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.adapter;
import android.content.Intent;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import net.schueller.peertube.R;
import net.schueller.peertube.activity.SearchServerActivity;
import net.schueller.peertube.helper.APIUrlHelper;
import net.schueller.peertube.model.Server;
import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
import org.apache.maven.artifact.versioning.DefaultArtifactVersion;
import static android.app.Activity.RESULT_OK;
public class ServerSearchAdapter extends RecyclerView.Adapter<ServerSearchAdapter.AccountViewHolder> {
private ArrayList<Server> serverList;
private SearchServerActivity activity;
private String baseUrl;
public ServerSearchAdapter(ArrayList<Server> serverList, SearchServerActivity activity) {
this.serverList = serverList;
this.activity = activity;
}
@NonNull
@Override
public AccountViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
View view = layoutInflater.inflate(R.layout.row_search_server, parent, false);
baseUrl = APIUrlHelper.getUrl(activity);
return new AccountViewHolder(view);
}
@Override
public void onBindViewHolder(@NonNull AccountViewHolder holder, int position) {
holder.name.setText(serverList.get(position).getName());
holder.host.setText(serverList.get(position).getHost());
holder.signupAllowed.setText(activity.getString(R.string.server_selection_signup_allowed, activity.getString(
serverList.get(position).getSignupAllowed() ?
R.string.server_selection_signup_allowed_yes :
R.string.server_selection_signup_allowed_no
)));
holder.videoTotals.setText(
activity.getString(R.string.server_selection_video_totals,
serverList.get(position).getTotalVideos().toString(),
serverList.get(position).getTotalLocalVideos().toString()
));
// don't show description if it hasn't been changes from the default
if (!activity.getString(R.string.peertube_instance_search_default_description).equals(serverList.get(position).getShortDescription())) {
holder.shortDescription.setText(serverList.get(position).getShortDescription());
holder.shortDescription.setVisibility(View.VISIBLE);
} else {
holder.shortDescription.setVisibility(View.GONE);
}
DefaultArtifactVersion serverVersion = new DefaultArtifactVersion(serverList.get(position).getVersion());
// at least version 2.2
DefaultArtifactVersion minVersion22 = new DefaultArtifactVersion("2.2.0");
if (serverVersion.compareTo(minVersion22) >= 0) {
// show NSFW Icon
if (serverList.get(position).getNSFW()) {
holder.isNSFW.setVisibility(View.VISIBLE);
}
}
// select server
holder.itemView.setOnClickListener(v -> {
String serverUrl = APIUrlHelper.cleanServerUrl(serverList.get(position).getHost());
Toast.makeText(activity, activity.getString(R.string.server_selection_set_server, serverUrl), Toast.LENGTH_LONG).show();
Intent intent = new Intent();
intent.putExtra("serverUrl", serverUrl);
intent.putExtra("serverName", serverList.get(position).getName());
activity.setResult(RESULT_OK, intent);
activity.finish();
});
//
//
// holder.moreButton.setText(R.string.video_more_icon);
// new Iconics.Builder().on(holder.moreButton).build();
//
// holder.moreButton.setOnClickListener(v -> {
//
// PopupMenu popup = new PopupMenu(context, v);
// popup.setOnMenuItemClickListener(menuItem -> {
// switch (menuItem.getItemId()) {
// case R.id.menu_share:
// Intents.Share(context, serverList.get(position));
// return true;
// default:
// return false;
// }
// });
// popup.inflate(R.menu.menu_video_row_mode);
// popup.show();
//
// });
}
public void setData(ArrayList<Server> data) {
serverList.addAll(data);
this.notifyDataSetChanged();
}
public void clearData() {
serverList.clear();
this.notifyDataSetChanged();
}
@Override
public int getItemCount() {
return serverList.size();
}
static class AccountViewHolder extends RecyclerView.ViewHolder {
TextView name, host, signupAllowed, shortDescription, videoTotals;
ImageView isNSFW;
AccountViewHolder(View itemView) {
super(itemView);
name = itemView.findViewById(R.id.sl_row_name);
host = itemView.findViewById(R.id.sl_row_host);
signupAllowed = itemView.findViewById(R.id.sl_row_signup_allowed);
shortDescription = itemView.findViewById(R.id.sl_row_short_description);
isNSFW = itemView.findViewById(R.id.sl_row_is_nsfw);
videoTotals = itemView.findViewById(R.id.sl_row_video_totals);
}
}
}

View File

@ -1,34 +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.application;
import android.app.Application;
import android.content.Context;
public class AppApplication extends Application {
private static Application instance;
@Override
public void onCreate() {
super.onCreate();
instance = this;
}
public static Context getContext() {
return instance.getApplicationContext();
}
}

View File

@ -1,27 +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.database;
import androidx.room.Database;
import androidx.room.RoomDatabase;
@Database(entities = {Server.class, Video.class}, version = 1)
public abstract class AppDatabase extends RoomDatabase {
public abstract ServerDao serverDao();
public abstract VideoDao videoDao();
}

View File

@ -1,44 +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.database
import android.os.Parcelable
import androidx.room.PrimaryKey
import androidx.room.ColumnInfo
import androidx.room.Entity
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "server_table")
data class Server(
@PrimaryKey(autoGenerate = true)
var id: Int = 0,
@ColumnInfo(name = "server_name")
var serverName: String,
@ColumnInfo(name = "server_host")
var serverHost: String? = null,
@ColumnInfo(name = "username")
var username: String? = null,
@ColumnInfo(name = "password")
var password: String? = null
) : Parcelable

View File

@ -1,39 +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.database
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface ServerDao {
@Insert
suspend fun insert(server: Server)
@Update
suspend fun update(server: Server)
@Query("DELETE FROM server_table")
suspend fun deleteAll()
@Delete
suspend fun delete(server: Server)
@get:Query("SELECT * from server_table ORDER BY server_name DESC")
val allServers: LiveData<List<Server>>
}

View File

@ -1,47 +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.database
import android.app.Application
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class ServerRepository(application: Application) {
private val mServerDao: ServerDao
val allServers: LiveData<List<Server>>
get() = mServerDao.allServers
init {
val db = ServerRoomDatabase.getDatabase(application)
mServerDao = db.serverDao()
}
suspend fun update(server: Server) = withContext(Dispatchers.IO) {
mServerDao.update(server)
}
suspend fun insert(server: Server) = withContext(Dispatchers.IO) {
mServerDao.insert(server)
}
suspend fun delete(server: Server) = withContext(Dispatchers.IO){
mServerDao.delete(server)
}
}

View File

@ -1,56 +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.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {Server.class}, version = 1, exportSchema = false)
public abstract class ServerRoomDatabase extends RoomDatabase {
public abstract ServerDao serverDao();
private static volatile ServerRoomDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
public static ServerRoomDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (ServerRoomDatabase.class) {
if (INSTANCE == null) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
ServerRoomDatabase.class, "server_database")
.build();
}
}
}
}
return INSTANCE;
}
}

View File

@ -1,47 +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.database
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class ServerViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: ServerRepository = ServerRepository(application)
val allServers: LiveData<List<Server>> = mRepository.allServers
fun insert(server: Server) {
viewModelScope.launch {
mRepository.insert(server)
}
}
fun update(server: Server) {
viewModelScope.launch {
mRepository.update(server)
}
}
fun delete(server: Server) {
viewModelScope.launch {
mRepository.delete(server)
}
}
}

View File

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

View File

@ -1,39 +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.database
import androidx.lifecycle.LiveData
import androidx.room.*
@Dao
interface VideoDao {
@Insert
suspend fun insert(video: Video)
@Update
suspend fun update(video: Video)
@Query("DELETE FROM watch_later")
suspend fun deleteAll()
@Delete
suspend fun delete(video: Video)
@get:Query("SELECT * from watch_later ORDER BY video_name DESC")
val allVideos: LiveData<List<Video>>
}

View File

@ -1,47 +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.database
import android.app.Application
import androidx.lifecycle.LiveData
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
internal class VideoRepository(application: Application) {
private val mVideoDao: VideoDao
val allVideos: LiveData<List<Video>>
get() = mVideoDao.allVideos
init {
val db = VideoRoomDatabase.getDatabase(application)
mVideoDao = db.videoDao()
}
suspend fun update(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.update(video)
}
suspend fun insert(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.insert(video)
}
suspend fun delete(video: Video) = withContext(Dispatchers.IO) {
mVideoDao.delete(video)
}
}

View File

@ -1,54 +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.database;
import android.content.Context;
import androidx.room.Database;
import androidx.room.Room;
import androidx.room.RoomDatabase;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Database(entities = {Video.class}, version = 1, exportSchema = false)
public abstract class VideoRoomDatabase extends RoomDatabase {
public abstract VideoDao videoDao();
private static volatile VideoRoomDatabase INSTANCE;
private static final int NUMBER_OF_THREADS = 4;
static final ExecutorService databaseWriteExecutor =
Executors.newFixedThreadPool(NUMBER_OF_THREADS);
public static VideoRoomDatabase getDatabase(final Context context) {
if (INSTANCE == null) {
synchronized (VideoRoomDatabase.class) {
if (INSTANCE == null) {
if (INSTANCE == null) {
INSTANCE = Room.databaseBuilder(context.getApplicationContext(),
VideoRoomDatabase.class, "playlist_database")
.build();
}
}
}
}
return INSTANCE;
}
}

View File

@ -1,47 +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.database
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.LiveData
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class VideoViewModel(application: Application) : AndroidViewModel(application) {
private val mRepository: VideoRepository = VideoRepository(application)
val allVideos: LiveData<List<Video>> = mRepository.allVideos
fun insert(video: Video) {
viewModelScope.launch {
mRepository.insert(video)
}
}
fun update(video: Video) {
viewModelScope.launch {
mRepository.update(video)
}
}
fun delete(video: Video) {
viewModelScope.launch {
mRepository.delete(video)
}
}
}

View File

@ -1,19 +0,0 @@
package net.schueller.peertube.feature_server_address.domain.use_case
import net.schueller.peertube.feature_server_address.domain.model.InvalidServerAddressException
import net.schueller.peertube.feature_server_address.domain.model.ServerAddress
import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository
class AddServerAddress(
private val repository: ServerAddressRepository
) {
@Throws(InvalidServerAddressException::class)
suspend operator fun invoke(serverAddress: ServerAddress) {
if(serverAddress.serverName.isBlank()) {
throw InvalidServerAddressException("Server Name is required")
}
repository.insertServerAddress(serverAddress)
}
}

View File

@ -1,13 +0,0 @@
package net.schueller.peertube.feature_server_address.domain.use_case
import net.schueller.peertube.feature_server_address.domain.model.ServerAddress
import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository
class DeleteServerAddress(
private val repository: ServerAddressRepository
) {
suspend operator fun invoke(serverAddress: ServerAddress) {
repository.deleteServerAddress(serverAddress)
}
}

View File

@ -1,13 +0,0 @@
package net.schueller.peertube.feature_server_address.domain.use_case
import net.schueller.peertube.feature_server_address.domain.model.ServerAddress
import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository
class GetServerAddress(
private val repository: ServerAddressRepository
) {
suspend operator fun invoke(id: Int): ServerAddress? {
return repository.getServerAddressById(id)
}
}

View File

@ -1,34 +0,0 @@
package net.schueller.peertube.feature_server_address.domain.use_case
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import net.schueller.peertube.feature_server_address.domain.model.ServerAddress
import net.schueller.peertube.feature_server_address.domain.repository.ServerAddressRepository
import net.schueller.peertube.feature_server_address.domain.util.OrderType
import net.schueller.peertube.feature_server_address.domain.util.ServerAddressOrder
class GetServerAddresses(
private val repository: ServerAddressRepository
) {
operator fun invoke(
serverAddressOrder: ServerAddressOrder = ServerAddressOrder.Title(OrderType.Descending)
): Flow<List<ServerAddress>> {
return repository.getServerAddresses().map { serverAddresses ->
when(serverAddressOrder.orderType) {
is OrderType.Ascending -> {
when(serverAddressOrder) {
is ServerAddressOrder.Title -> serverAddresses.sortedBy { it.serverName.lowercase() }
is ServerAddressOrder.Host -> serverAddresses.sortedBy { it.serverHost?.lowercase() }
}
}
is OrderType.Descending -> {
when(serverAddressOrder) {
is ServerAddressOrder.Title -> serverAddresses.sortedByDescending { it.serverName.lowercase() }
is ServerAddressOrder.Host -> serverAddresses.sortedByDescending { it.serverHost?.lowercase() }
}
}
}
}
}
}

View File

@ -1,38 +0,0 @@
package net.schueller.peertube.feature_server_address.domain.use_case
import android.content.Context
import dagger.hilt.android.qualifiers.ApplicationContext
import net.schueller.peertube.common.Constants.PREF_API_BASE_KEY
import net.schueller.peertube.feature_server_address.domain.model.ServerAddress
import net.schueller.peertube.feature_video.data.remote.auth.LoginService
import net.schueller.peertube.feature_video.data.remote.auth.Session
class SelectServerAddress (
@ApplicationContext private val context: Context,
private val session: Session,
private val loginService: LoginService
) {
private val sharedPreferences = context.getSharedPreferences(context.packageName + "_preferences", Context.MODE_PRIVATE)
operator fun invoke(serverAddress: ServerAddress) {
// save new server to pref
val editor = sharedPreferences.edit()
editor.putString(PREF_API_BASE_KEY, serverAddress.serverHost)
editor.apply()
// invalidate session
if (session.isLoggedIn()) {
session.invalidate()
}
// attempt auth if we have username
if (serverAddress.username.isNullOrBlank().not()) {
loginService.authenticate(serverAddress.username, serverAddress.password)
}
// TODO: notify views that this has changed
}
}

View File

@ -1,46 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import net.schueller.peertube.common.Constants.VIDEOS_API_PAGE_SIZE
import net.schueller.peertube.feature_video.domain.model.Overview
import net.schueller.peertube.feature_video.domain.repository.VideoRepository
import net.schueller.peertube.feature_video.domain.source.ExplorePagingSource
import javax.inject.Inject
@HiltViewModel
class VideoExploreViewModel @Inject constructor(
private val repository: VideoRepository
) : ViewModel() {
private val _videos = MutableStateFlow<PagingData<Overview>>(PagingData.empty())
val videos = _videos
init {
getVideos()
}
private fun getVideos() {
viewModelScope.launch {
Pager(
PagingConfig(
pageSize = 1,
maxSize = 5
)
) {
ExplorePagingSource(repository)
}.flow.cachedIn(viewModelScope).collect {
_videos.value = it
}
}
}
}

View File

@ -1,13 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list
const val SET_DISCOVER = "discover"
const val SET_LOCAL = "local"
const val SET_RECENT = "recent"
const val SET_TRENDING = "trending"
const val SET_SUBSCRIPTIONS = "subscriptions"
sealed class VideoListEvent {
data class UpdateQuery(
val set: String?
): VideoListEvent()
}

View File

@ -1,234 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list
import android.util.Log
import androidx.compose.foundation.layout.*
import androidx.compose.runtime.Composable
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.material.Scaffold
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.NestedScrollSource
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.paging.LoadState
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import coil.annotation.ExperimentalCoilApi
import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import kotlinx.coroutines.flow.collectLatest
import net.schueller.peertube.feature_video.domain.model.Overview
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.presentation.video_list.components.*
import net.schueller.peertube.presentation.Screen
import kotlin.math.roundToInt
@ExperimentalCoilApi
@Composable
fun VideoListScreen(
navController: NavController,
viewModel: VideoListViewModel = hiltViewModel(),
viewExploreModel: VideoExploreViewModel = hiltViewModel()
) {
val state = viewModel.state.value
val lazyVideoExploreItems: LazyPagingItems<Overview> = viewExploreModel.videos.collectAsLazyPagingItems()
val lazyVideoItems: LazyPagingItems<Video> = viewModel.videos.collectAsLazyPagingItems()
val listState = rememberLazyListState()
// Events
LaunchedEffect(key1 = true) {
viewModel.eventFlow.collectLatest { event ->
when(event) {
is VideoListViewModel.UiEvent.ScrollToTop -> {
listState.scrollToItem(index = 0)
}
}
}
}
// Auto hide top appbar
val toolBarHeight = 56.dp
val toolBarHeightPx = with(LocalDensity.current) { toolBarHeight.roundToPx().toFloat()}
val toolBarOffsetHeightPx = remember { mutableStateOf(0f) }
val nestedScrollConnection = remember {
object : NestedScrollConnection {
override fun onPreScroll(available: Offset, source: NestedScrollSource): Offset {
val delta = available.y
val newOffset = toolBarOffsetHeightPx.value + delta
toolBarOffsetHeightPx.value = newOffset.coerceIn(-toolBarHeightPx, 0f)
return Offset.Zero
}
}
}
Box(
Modifier
.fillMaxSize()
.nestedScroll(nestedScrollConnection)
)
{
Scaffold(
bottomBar = {
BottomBarComponent(navController)
}
) {
SwipeRefresh(
state = rememberSwipeRefreshState(
isRefreshing = (lazyVideoItems.loadState.refresh is LoadState.Loading) || (lazyVideoExploreItems.loadState.refresh is LoadState.Loading)
),
onRefresh = {
if (state.explore) {
lazyVideoExploreItems.refresh()
} else {
lazyVideoItems.refresh()
}
}
) {
LazyColumn(
contentPadding = PaddingValues(top = toolBarHeight),
state = listState,
modifier = Modifier.fillMaxSize()
) {
if (state.explore) {
itemsIndexed(lazyVideoExploreItems) { _, overview ->
if (overview != null) {
// Categories
if (overview.categories?.isNotEmpty() == true) {
overview.categories.forEach { categoryVideo ->
VideoCategory(categoryVideo.category)
if (categoryVideo.videos.isNotEmpty()) {
categoryVideo.videos.forEach { video ->
VideoListItem(
video = video,
onItemClick = {
navController.navigate(Screen.VideoPlayScreen.route + "/${video.uuid}")
}
)
}
}
}
}
// Channels
if (overview.channels?.isNotEmpty() == true) {
overview.channels.forEach { channelVideo ->
VideoChannel(channelVideo.channel)
if (channelVideo.videos.isNotEmpty()) {
channelVideo.videos.forEach { video ->
VideoListItem(
video = video,
onItemClick = {
navController.navigate(Screen.VideoPlayScreen.route + "/${video.uuid}")
}
)
}
}
}
}
// Tags
if (overview.tags?.isNotEmpty() == true) {
overview.tags.forEach { tagVideo ->
VideoTag(tagVideo.tag)
if (tagVideo.videos.isNotEmpty()) {
tagVideo.videos.forEach { video ->
VideoListItem(
video = video,
onItemClick = {
navController.navigate(Screen.VideoPlayScreen.route + "/${video.uuid}")
}
)
}
}
}
}
}
}
} else {
itemsIndexed(lazyVideoItems) { item, video ->
if (video != null) {
Log.v("VLV", video.id.toString() + "-" + item.toString())
VideoListItem(
video = video,
onItemClick = {
navController.navigate(Screen.VideoPlayScreen.route + "/${video.uuid}")
}
)
}
}
lazyVideoItems.apply {
when {
loadState.refresh is LoadState.Loading -> {
item {
// LoadingView(modifier = Modifier.fillParentMaxSize())
}
}
loadState.append is LoadState.Loading -> {
item {
// LoadingItem()
}
}
loadState.refresh is LoadState.Error -> {
val e = lazyVideoItems.loadState.refresh as LoadState.Error
item {
// ErrorItem(
// message = e.error.localizedMessage!!,
// modifier = Modifier.fillParentMaxSize(),
// onClickRetry = { retry() }
// )
}
}
loadState.append is LoadState.Error -> {
val e = lazyVideoItems.loadState.append as LoadState.Error
item {
// ErrorItem(
// message = e.error.localizedMessage!!,
// onClickRetry = { retry() }
// )
}
}
}
}
}
// TODO: deal with errors, https://proandroiddev.com/infinite-lists-with-paging-3-in-jetpack-compose-b095533aefe6
}
}
// Place after list, so it floats above the list in z-height
TopAppBarComponent(
navController,
modifier = Modifier
.height(toolBarHeight)
.offset {
IntOffset(x = 0, y = toolBarOffsetHeightPx.value.roundToInt())
}
)
// if(error) {
// Text(
// text = "Error Text",
// color = MaterialTheme.colors.error,
// textAlign = TextAlign.Center,
// modifier = Modifier
// .fillMaxWidth()
// .padding(horizontal = 20.dp)
// .align(Alignment.Center)
// )
// }
}
}
}

View File

@ -1,11 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list
data class VideoListState(
val sort: String? = null,
val filter: String? = null,
val nsfw: String? = null,
val languages: Set<String?>? = null,
val explore: Boolean = false,
val local: Boolean = false,
val subscriptions: Boolean = false
)

View File

@ -1,123 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list
import android.util.Log
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import net.schueller.peertube.common.Constants.VIDEOS_API_PAGE_SIZE
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.domain.repository.VideoRepository
import net.schueller.peertube.feature_video.domain.source.VideoPagingSource
import javax.inject.Inject
@HiltViewModel
class VideoListViewModel @Inject constructor(
private val repository: VideoRepository
) : ViewModel() {
private val _state = mutableStateOf(VideoListState())
val state: State<VideoListState> = _state
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
private val _videos = MutableStateFlow<PagingData<Video>>(PagingData.empty())
val videos = _videos
init {
Log.v("VLM", "INIT")
getVideos()
}
private fun getVideos() {
viewModelScope.launch {
Pager(
PagingConfig(
pageSize = VIDEOS_API_PAGE_SIZE,
maxSize = 100
)
) {
VideoPagingSource(
repository,
_state.value.sort,
_state.value.nsfw,
_state.value.filter,
_state.value.languages
)
}.flow.cachedIn(viewModelScope).collect {
_videos.value = it
}
}
}
fun onEvent(event: VideoListEvent) {
when (event) {
is VideoListEvent.UpdateQuery -> {
viewModelScope.launch {
when (event.set) {
SET_DISCOVER -> {
_state.value = VideoListState(
explore = true,
local = false,
subscriptions = false,
)
}
SET_TRENDING -> {
_state.value = VideoListState(
sort = "-trending",
explore = false,
local = false,
subscriptions = false
)
}
SET_RECENT -> {
_state.value = VideoListState(
sort = "-createdAt",
explore = false,
local = false,
subscriptions = false
)
}
SET_LOCAL -> {
_state.value = VideoListState(
sort = "-publishedAt",
explore = false,
local = true,
subscriptions = false
)
}
SET_SUBSCRIPTIONS -> {
_state.value = VideoListState(
explore = false,
local = false,
subscriptions = true
)
}
}
getVideos()
Log.v("vvm", "Update sort: " + event.set)
_eventFlow.emit(UiEvent.ScrollToTop)
}
}
}
}
sealed class UiEvent {
object ScrollToTop : UiEvent()
}
}

View File

@ -1,87 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import net.schueller.peertube.feature_video.presentation.video_list.VideoListEvent
import net.schueller.peertube.feature_video.presentation.video_list.VideoListViewModel
@Composable
fun BottomBarComponent(
navController: NavController
) {
BottomAppBar(
content = {
BottomNavigationBar(navController)
}
)
}
@Composable
fun BottomNavigationBar(
navController: NavController,
videoListViewModel: VideoListViewModel = hiltViewModel()
) {
val items = listOf(
BottomBarItems.Discover,
BottomBarItems.Trending,
BottomBarItems.Recent,
BottomBarItems.Local,
BottomBarItems.Subscriptions
)
BottomNavigation(
contentColor = Color.White
) {
// val navBackStackEntry by navController.currentBackStackEntryAsState()
// val currentRoute = navBackStackEntry?.destination?.route
items.forEach { item ->
BottomNavigationItem(
// modifier = Modifier.width(105.dp),
icon = {
Icon(painterResource(id = item.icon),
contentDescription = item.title
)
},
label = {
Text(
text = item.title,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
},
selectedContentColor = Color.White,
unselectedContentColor = Color.White.copy(0.75f),
alwaysShowLabel = true,
selected = false, //currentRoute == item.route,
onClick = {
videoListViewModel.onEvent(
VideoListEvent.UpdateQuery(
set = item.set
)
)
navController.navigate(item.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = false
}
}
)
}
}
}

View File

@ -1,46 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components
import net.schueller.peertube.R
import net.schueller.peertube.feature_video.presentation.video_list.*
sealed class BottomBarItems(
var route: String,
var icon: Int,
var title: String,
var set: String
) {
object Discover : BottomBarItems(
"video_list_screen",
R.drawable.ic_globe,
"Discover",
SET_DISCOVER
)
object Trending : BottomBarItems(
"video_list_screen",
R.drawable.ic_trending_up,
"Trending",
SET_TRENDING
)
object Recent : BottomBarItems(
"video_list_screen",
R.drawable.ic_plus_circle,
"Recent",
SET_RECENT
)
object Local : BottomBarItems(
"video_list_screen",
R.drawable.ic_local,
"Local",
SET_LOCAL
)
object Subscriptions : BottomBarItems(
"settings_screen",
R.drawable.ic_subscriptions,
"Subscriptions",
SET_SUBSCRIPTIONS
)
}

View File

@ -1,56 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import coil.annotation.ExperimentalCoilApi
import net.schueller.peertube.R
import net.schueller.peertube.feature_video.presentation.me.MeViewModel
import net.schueller.peertube.feature_video.presentation.me.components.MeAvatar
@ExperimentalCoilApi
@Composable
fun TopAppBarComponent(
navController: NavController,
modifier: Modifier,
meViewModel: MeViewModel = hiltViewModel()
) {
TopAppBar(
modifier = modifier,
title = { Text(text = "AppBar") },
// color = Color.White,
actions = {
IconButton(onClick = {
navController.navigate("address_list") {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) {
saveState = true
}
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}) {
Icon(
painterResource(id = R.drawable.ic_server),
contentDescription = "Address Book"
)
}
MeAvatar(
avatar = meViewModel.stateMe.value.me?.account?.avatar,
onItemClick = {
navController.navigate("me_screen")
}
)
}
)
}

View File

@ -1,14 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components;
import androidx.compose.material.Text
import androidx.compose.runtime.Composable;
import net.schueller.peertube.feature_video.domain.model.Category
@Composable
fun VideoCategory(
category: Category
) {
Text(text = category.label)
}

View File

@ -1,13 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components;
import androidx.compose.material.Text
import androidx.compose.runtime.Composable;
import net.schueller.peertube.feature_video.domain.model.Channel
@Composable
fun VideoChannel(
channel: Channel
) {
Text(text = channel.name)
}

View File

@ -1,129 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.constraintlayout.widget.Placeholder
import coil.annotation.ExperimentalCoilApi
import coil.compose.ImagePainter
import coil.compose.rememberImagePainter
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
import net.schueller.peertube.R
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.presentation.common.*
@ExperimentalCoilApi
@Composable
fun VideoListItem(
video: Video,
onItemClick: (Video) -> Unit
) {
Card(
modifier = Modifier
.fillMaxWidth()
.wrapContentHeight()
.clickable { onItemClick(video) },
shape = RectangleShape,
) {
Column (
modifier = Modifier
.background(MaterialTheme.colors.surface)
.fillMaxWidth(),
// .clickable { onItemClick(video) }
) {
Box() {
val image = rememberImagePainter(
data = getImageUrl(video.previewPath),
// builder = {
// placeholder(R.drawable.test_image)
// }
)
Image(
painter = image,
contentDescription = null,
modifier = Modifier
.fillMaxWidth()
.height(240.dp)
.placeholder(
visible = (image.state is ImagePainter.State.Error || image.state is ImagePainter.State.Empty),
highlight = PlaceholderHighlight.fade()
),
contentScale = ContentScale.Crop
)
Box(
modifier = Modifier
.align(
Alignment.BottomEnd
)
.padding(2.dp)
) {
VideoTime(video)
}
}
// Video Meta
Row(
modifier = Modifier
.background(MaterialTheme.colors.surface)
.height(84.dp) // TODO: not setting this causes odd up scroll effect
) {
val avatar = rememberImagePainter(
data = getCreatorAvatarUrl(getCreatorAvatar(video))
)
Image(
painter = avatar,
contentDescription = null,
modifier = Modifier
.height(72.dp)
.width(72.dp)
.padding(12.dp)
.clip(shape = RoundedCornerShape(100.dp))
.placeholder(
visible = (avatar.state is ImagePainter.State.Error || avatar.state is ImagePainter.State.Empty),
highlight = if (avatar.state is ImagePainter.State.Empty) {
PlaceholderHighlight.fade()
} else {
null
},
),
contentScale = ContentScale.Crop
)
Column (
modifier = Modifier
.padding(6.dp),
)
{
Text(
text = video.name ?: "No Name",
fontWeight = FontWeight.Bold,
)
Text(
text = getMetaDataTag(video.createdAt, video.views, "",true),
fontWeight = FontWeight.Normal,
style = MaterialTheme.typography.caption
)
Text(
text = getCreatorString(video, true),
fontWeight = FontWeight.Normal,
style = MaterialTheme.typography.caption
)
}
}
}
}
}

View File

@ -1,11 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components;
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
@Composable
fun VideoTag(
tag: String
) {
Text(text = tag)
}

View File

@ -1,93 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_list.components
import android.text.format.DateUtils
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Icon
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import net.schueller.peertube.R
import net.schueller.peertube.feature_video.domain.model.Video
@Composable
fun VideoTime(
video: Video
) {
val backgroundColor = if (video.isLive) {
Color(
red = 0xFF,
blue = 0,
green = 0,
alpha = 0xCC
)
} else {
Color(
red = 0,
blue = 0,
green = 0,
alpha = 0x99
)
}
val timeStamp = if (video.isLive) {
"LIVE"
} else {
getDuration(video.duration.toLong())
}
Box(
modifier = Modifier
.wrapContentHeight()
.background(
color = backgroundColor
)
) {
Row() {
if (video.isLive) {
Icon(
painterResource(id = R.drawable.ic_radio),
contentDescription = "signupAllowed",
modifier = Modifier.requiredSize(18.dp)
.align(CenterVertically)
.padding(
start = 4.dp
),
tint = Color(
red = 0xFF,
blue = 0xFF,
green = 0xFF,
alpha = 0xFF
)
)
}
Text(
modifier = Modifier.padding(
end = 4.dp,
start = 4.dp
),
color = Color(
red = 0xFF,
blue = 0xFF,
green = 0xFF,
alpha = 0xFF
),
text = timeStamp,
fontWeight = FontWeight.Bold,
fontSize = 12.sp
)
}
}
}
@Composable
fun getDuration(duration: Long?): String {
return DateUtils.formatElapsedTime(duration!!)
}

View File

@ -1,9 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play
import net.schueller.peertube.feature_video.domain.model.Description
data class VideoDescriptionState(
val isLoading: Boolean = false,
val description: Description? = null,
val error: String = ""
)

View File

@ -1,16 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play
import net.schueller.peertube.feature_video.domain.model.Video
sealed class VideoPlayEvent {
data class ShareVideo(val video: Video): VideoPlayEvent()
data class UpVoteVideo(val video: Video): VideoPlayEvent()
data class DownVoteVideo(val video: Video): VideoPlayEvent()
data class DownloadVideo(val video: Video): VideoPlayEvent()
data class AddVideoToPlaylist(val video: Video): VideoPlayEvent()
data class FlagVideo(val video: Video): VideoPlayEvent()
data class BlockVideo(val video: Video): VideoPlayEvent()
data class OpenDescription(val video: Video): VideoPlayEvent()
object CloseDescription: VideoPlayEvent()
}

View File

@ -1,216 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play
import android.content.res.Configuration
import android.widget.Toast
import androidx.compose.animation.*
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavController
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.itemsIndexed
import com.google.accompanist.systemuicontroller.SystemUiController
import com.google.accompanist.systemuicontroller.rememberSystemUiController
import kotlinx.coroutines.flow.collectLatest
import net.schueller.peertube.feature_server_address.presentation.address_add_edit.AddEditAddressViewModel
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.presentation.video_list.components.VideoListItem
import net.schueller.peertube.feature_video.presentation.video_play.components.VideoDescriptionScreen
import net.schueller.peertube.feature_video.presentation.video_play.components.VideoMeta
import net.schueller.peertube.feature_video.presentation.video_play.components.VideoScreen
import net.schueller.peertube.feature_video.presentation.video_play.player.ExoPlayerHolder
import net.schueller.peertube.presentation.Screen
var enteringPIPMode: Boolean = false
@ExperimentalMaterialApi
@Composable
fun VideoPlayScreen(
exoPlayerHolder: ExoPlayerHolder,
navController: NavController,
viewModel: VideoPlayViewModel = hiltViewModel()
) {
val state = viewModel.state.value
val context = LocalContext.current
var descriptionVisible by remember { mutableStateOf(false) }
// Show toasts
LaunchedEffect(key1 = true) {
viewModel.eventFlow.collectLatest { event ->
when(event) {
is VideoPlayViewModel.UiEvent.ShowToast -> {
Toast.makeText(
context,
event.message,
event.length
).show()
}
is VideoPlayViewModel.UiEvent.ShowDescription -> {
descriptionVisible = true
}
is VideoPlayViewModel.UiEvent.HideDescription -> {
descriptionVisible = false
}
}
}
}
// Related Videos
val lazyRelatedVideoItems: LazyPagingItems<Video> = viewModel.relatedVideos.collectAsLazyPagingItems()
Box(modifier = Modifier
.fillMaxSize()
.background(Color.Black)) {
val configuration = LocalConfiguration.current
state.video?.let { video ->
val systemUiController: SystemUiController = rememberSystemUiController()
when(configuration.orientation) {
Configuration.ORIENTATION_LANDSCAPE -> {
// Full screen
systemUiController.isStatusBarVisible = false
systemUiController.isNavigationBarVisible = false
systemUiController.isSystemBarsVisible = false
VideoScreen(exoPlayerHolder, video)
} else -> {
systemUiController.isStatusBarVisible = true
systemUiController.isNavigationBarVisible = true
systemUiController.isSystemBarsVisible = true
// TODO: Swipe video down
val squareSize = 200.dp
val swipeableState = rememberSwipeableState(initialValue = 0)
val sizePx = with(LocalDensity.current) { squareSize.toPx() }
val anchors = mapOf(sizePx to 1, 0f to 0)
Column(
modifier = Modifier
.fillMaxWidth()
.swipeable(
state = swipeableState,
anchors = anchors,
thresholds = { _, _ -> FractionalThreshold(0.3f) },
orientation = Orientation.Vertical,
)
) {
VideoScreen(exoPlayerHolder, video)
Box() {
LazyColumn(
modifier = Modifier.fillMaxSize()
) {
item {
VideoMeta(video)
}
itemsIndexed(lazyRelatedVideoItems) { _, video ->
if (video != null) {
VideoListItem(
video = video,
onItemClick = {
navController.navigate(Screen.VideoPlayScreen.route + "/${video.uuid}")
}
)
}
}
}
Column(Modifier.fillMaxSize()) {
AnimatedVisibility(
visible = descriptionVisible,
modifier = Modifier.fillMaxSize(),
enter = slideInVertically(
initialOffsetY = { it }, // it == fullWidth
animationSpec = tween(
durationMillis = 150,
easing = LinearEasing
)
),
exit = slideOutVertically(
targetOffsetY = { it },
animationSpec = tween(
durationMillis = 150,
easing = LinearEasing
)
)
) {
VideoDescriptionScreen()
}
}
}
}
}
}
}
if(state.error.isNotBlank()) {
Text(
text = state.error,
color = MaterialTheme.colors.error,
textAlign = TextAlign.Center,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
.align(Alignment.Center)
)
}
if(state.isLoading) {
CircularProgressIndicator(modifier = Modifier.align(Alignment.Center))
}
}
// BackHandler(enabled = true) {
// Log.v("back", "back pressed")
//
// enterPIPMode(activity)
// }
}
//fun enterPIPMode(activity: Activity): Boolean {
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
// if (enteringPIPMode) {
// return true
// }
// if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
// ) {
// enteringPIPMode = true
// val params = PictureInPictureParams.Builder().build()
// try {
// activity.enterPictureInPictureMode(params)
// return true
// } catch (ex: IllegalStateException) {
// // pass
// enteringPIPMode = false
// }
// }
// }
// return false
//}

View File

@ -1,12 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play
import net.schueller.peertube.feature_video.domain.model.Description
import net.schueller.peertube.feature_video.domain.model.Rating
import net.schueller.peertube.feature_video.domain.model.Video
data class VideoPlayState(
val isLoading: Boolean = false,
val video: Video? = null,
val rating: Rating? = null,
val error: String = ""
)

View File

@ -1,217 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play
import android.widget.Toast
import androidx.compose.runtime.State
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import androidx.paging.cachedIn
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.launch
import net.schueller.peertube.common.Constants
import net.schueller.peertube.common.Resource
import net.schueller.peertube.feature_video.data.remote.auth.Session
import net.schueller.peertube.feature_video.domain.model.Description
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.domain.repository.VideoRepository
import net.schueller.peertube.feature_video.domain.source.VideoPagingSource
import net.schueller.peertube.feature_video.domain.source.PlaylistVideoPagingSource
import net.schueller.peertube.feature_video.domain.use_case.*
import javax.inject.Inject
@HiltViewModel
class VideoPlayViewModel @Inject constructor(
private val getVideoUseCase: GetVideoUseCase,
private val upVoteVideoUseCase: UpVoteVideoUseCase,
private val downVoteVideoUseCase: DownVoteVideoUseCase,
private val shareVideoUseCase: ShareVideoUseCase,
private val downloadVideoUseCase: DownloadVideoUseCase,
private val addVideoToPlaylistUseCase: AddVideoToPlaylistUseCase,
private val blockVideoUseCase: BlockVideoUseCase,
private val flagVideoUseCase: FlagVideoUseCase,
private val getVideoRatingUseCase: GetVideoRatingUseCase,
private val getVideoDescriptionUseCase: GetVideoDescriptionUseCase,
private val session: Session,
private val repository: VideoRepository,
savedStateHandle: SavedStateHandle
) : ViewModel() {
private val _state = mutableStateOf(VideoPlayState())
val state: State<VideoPlayState> = _state
private val _stateVideoDescription = mutableStateOf(VideoDescriptionState())
val stateVideoDescription: State<VideoDescriptionState> = _stateVideoDescription
private val _eventFlow = MutableSharedFlow<UiEvent>()
val eventFlow = _eventFlow.asSharedFlow()
init {
savedStateHandle.get<String>(Constants.PARAM_VIDEO_UUID)?.let { uuid ->
getVideo(uuid)
getDescription(uuid)
}
}
var relatedVideos: Flow<PagingData<Video>> = Pager(
PagingConfig(
pageSize = Constants.VIDEOS_API_PAGE_SIZE,
maxSize = 100
)
) {
// if (session.isLoggedIn()) {
// PlaylistVideoPagingSource(repository, "-publishedAt", video)
// } else {
VideoPagingSource(repository, "-publishedAt", null, null ,null)
// }
}.flow.cachedIn(viewModelScope)
private fun getDescription(uuid: String) {
// get description data
getVideoDescriptionUseCase(uuid).onEach { result ->
when (result) {
is Resource.Success -> {
_stateVideoDescription.value = VideoDescriptionState(description = result.data)
}
is Resource.Error -> {
_stateVideoDescription.value = VideoDescriptionState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.Loading -> {
_stateVideoDescription.value = VideoDescriptionState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
private fun getVideo(uuid: String) {
getVideoUseCase(uuid).onEach { result ->
when (result) {
is Resource.Success -> {
_state.value = VideoPlayState(video = result.data)
// Add short description
_stateVideoDescription.value = VideoDescriptionState(description = Description(description = result.data?.description ?: "") )
if (result.data != null) {
getRating(result.data.id)
}
}
is Resource.Error -> {
_state.value = VideoPlayState(
error = result.message ?: "An unexpected error occurred"
)
}
is Resource.Loading -> {
_state.value = VideoPlayState(isLoading = true)
}
}
}.launchIn(viewModelScope)
}
fun onEvent(event: VideoPlayEvent) {
when (event) {
is VideoPlayEvent.UpVoteVideo -> {
viewModelScope.launch {
if (session.isLoggedIn()) {
// TODO: must be logged in
upVoteVideoUseCase(event.video).onEach { result ->
when (result) {
is Resource.Success -> {
// Update rating
if (result.data != null) {
getRating(result.data.id)
}
}
else -> {
}
}
}.launchIn(viewModelScope)
} else {
_eventFlow.emit(UiEvent.ShowToast("You must be logged in", Toast.LENGTH_SHORT))
}
}
}
is VideoPlayEvent.DownVoteVideo -> {
viewModelScope.launch {
if (session.isLoggedIn()) {
// TODO: must be logged in
downVoteVideoUseCase(event.video).onEach { result ->
when (result) {
is Resource.Success -> {
// Update rating
if (result.data != null) {
getRating(result.data.id)
}
}
else -> {
}
}
}.launchIn(viewModelScope)
} else {
_eventFlow.emit(UiEvent.ShowToast("You must be logged in", Toast.LENGTH_SHORT))
}
}
}
is VideoPlayEvent.ShareVideo -> {
shareVideoUseCase(event.video)
}
is VideoPlayEvent.AddVideoToPlaylist -> {
addVideoToPlaylistUseCase(event.video)
}
is VideoPlayEvent.BlockVideo -> {
blockVideoUseCase(event.video)
}
is VideoPlayEvent.FlagVideo -> {
flagVideoUseCase(event.video)
}
is VideoPlayEvent.DownloadVideo -> {
// TODO: permissions
downloadVideoUseCase(event.video)
}
is VideoPlayEvent.OpenDescription -> {
// Show description before we have the data
viewModelScope.launch {
_eventFlow.emit(UiEvent.ShowDescription)
}
}
is VideoPlayEvent.CloseDescription -> {
viewModelScope.launch {
_eventFlow.emit(UiEvent.HideDescription)
}
}
}
}
private fun getRating(id: Int) {
getVideoRatingUseCase(id).onEach { res ->
when(res) {
is Resource.Success -> {
_state.value = VideoPlayState(rating = res.data)
}
else -> {
// error rating
}
}
}
}
sealed class UiEvent {
data class ShowToast(val message: String, val length: Int): UiEvent()
object ShowDescription : UiEvent()
object HideDescription : UiEvent()
}
}

View File

@ -1,132 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.components
import android.util.Log
import android.widget.ScrollView
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.horizontalScroll
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.material.ButtonDefaults
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.ViewModel
import com.google.android.material.internal.ContextUtils
import com.google.android.material.internal.ContextUtils.getActivity
import net.schueller.peertube.R
import net.schueller.peertube.feature_server_address.domain.model.Server
import net.schueller.peertube.feature_server_address.presentation.address_add_edit.AddEditAddressEvent
import net.schueller.peertube.feature_video.domain.model.RATING_DISLIKE
import net.schueller.peertube.feature_video.domain.model.RATING_LIKE
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.presentation.video_play.VideoPlayEvent
import net.schueller.peertube.feature_video.presentation.video_play.VideoPlayViewModel
@Composable
fun VideoActions(
video: Video,
viewModel: VideoPlayViewModel = hiltViewModel()
) {
val state = viewModel.state.value
Row(
modifier = Modifier
.padding(6.dp)
.fillMaxWidth()
.horizontalScroll(rememberScrollState())
) {
val thumbsUpIcon = if (state.rating?.rating === RATING_LIKE) {
R.drawable.ic_thumbs_up_filled
} else {
R.drawable.ic_thumbs_up
}
VideoAction(thumbsUpIcon, "Thumbs Up", video.likes.toString()) {
viewModel.onEvent(VideoPlayEvent.UpVoteVideo(video))
}
val thumbsDownIcon = if (state.rating?.rating === RATING_DISLIKE) {
R.drawable.ic_thumbs_down_filled
} else {
R.drawable.ic_thumbs_down
}
VideoAction(thumbsDownIcon, "Thumbs Down", video.dislikes.toString()) {
viewModel.onEvent(VideoPlayEvent.DownVoteVideo(video))
}
VideoAction(R.drawable.ic_share_2, "Share", "Share") {
viewModel.onEvent(VideoPlayEvent.ShareVideo(video))
}
if (video.downloadEnabled == true) {
VideoAction(R.drawable.ic_download, "Download", "Download") {
viewModel.onEvent(VideoPlayEvent.DownloadVideo(video))
}
}
VideoAction(R.drawable.ic_playlist_add, "Add to Playlist", "Add") {
viewModel.onEvent(VideoPlayEvent.AddVideoToPlaylist(video))
}
VideoAction(R.drawable.ic_slash, "Block", "Block") {
viewModel.onEvent(VideoPlayEvent.BlockVideo(video))
}
VideoAction(R.drawable.ic_flag, "Flag", "Flag") {
viewModel.onEvent(VideoPlayEvent.FlagVideo(video))
}
}
}
@Composable
fun VideoAction(
icon: Int,
iconText: String,
text: String,
onClick: () -> Unit
) {
Column(
modifier = Modifier
.padding(
top = 8.dp,
bottom = 8.dp,
start = 8.dp,
end = 8.dp
)
.width(56.dp)
.clickable(
onClick = onClick
),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painterResource(id = icon),
modifier = Modifier
.size(28.dp)
.padding(bottom = 4.dp),
contentDescription = iconText
)
Text(
text = text,
fontWeight = FontWeight.Normal,
style = MaterialTheme.typography.caption
)
}
}

View File

@ -1,49 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.components
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.*
import androidx.compose.material.Button
import androidx.compose.material.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import net.schueller.peertube.feature_video.presentation.video_play.VideoPlayEvent
import net.schueller.peertube.feature_video.presentation.video_play.VideoPlayViewModel
@Composable
fun VideoDescriptionScreen(
viewModel: VideoPlayViewModel = hiltViewModel()
) {
val state = viewModel.stateVideoDescription.value
Box(modifier = Modifier
.fillMaxSize()
.background(MaterialTheme.colors.background)
) {
state.description?.let { description ->
Column() {
Button(
onClick = {
viewModel.onEvent(VideoPlayEvent.CloseDescription)
}
) {
}
Text(
text = description.description,
color = MaterialTheme.colors.onBackground,
textAlign = TextAlign.Left,
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 20.dp)
)
}
}
}
}

View File

@ -1,117 +0,0 @@
//package net.schueller.peertube.feature_video.presentation.video_play.components
//
//import android.app.Activity
//import android.net.Uri
//import android.view.ViewGroup
//import android.widget.FrameLayout
//import androidx.compose.foundation.layout.fillMaxSize
//import androidx.compose.material.Surface
//import androidx.compose.runtime.Composable
//import androidx.compose.runtime.DisposableEffect
//import androidx.compose.runtime.LaunchedEffect
//import androidx.compose.runtime.remember
//import androidx.compose.ui.Modifier
//import androidx.compose.ui.platform.LocalContext
//import androidx.compose.ui.viewinterop.AndroidView
//import com.google.android.exoplayer2.C
//import com.google.android.exoplayer2.MediaItem
//import com.google.android.exoplayer2.source.ProgressiveMediaSource
//import com.google.android.exoplayer2.source.dash.DashMediaSource
//import com.google.android.exoplayer2.source.hls.HlsMediaSource
//import com.google.android.exoplayer2.ui.PlayerView
//import com.google.android.exoplayer2.util.Util
//import net.schueller.peertube.common.VideoHelper
//import net.schueller.peertube.feature_video.domain.model.Video
//import net.schueller.peertube.feature_video.presentation.video_play.player.DataSourceHolder
//import net.schueller.peertube.feature_video.presentation.video_play.player.PlayerViewPool
//import net.schueller.peertube.feature_video.presentation.video_play.player.ExoPlayerHolder
//
//
//@Composable
//fun VideoItem(
// video: Video,
// modifier: Modifier
//) {
// Surface(
// modifier = modifier
// ) {
// val videoHelper = VideoHelper()
//
// val context = LocalContext.current
// val activity = context as Activity
// val exoPlayer = remember { ExoPlayerHolder.get(context) }
// var playerView: PlayerView? = null
//
// LaunchedEffect(videoHelper.pickPlaybackResolution(video)) {
// val videoUri = Uri.parse(videoHelper.pickPlaybackResolution(video))
// val dataSourceFactory = DataSourceHolder.getCacheFactory(context)
// val source = when (Util.inferContentType(videoUri)) {
// C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory)
// .createMediaSource(MediaItem.fromUri(videoUri))
// C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory)
// .createMediaSource(MediaItem.fromUri(videoUri))
// else -> ProgressiveMediaSource.Factory(dataSourceFactory)
// .createMediaSource(MediaItem.fromUri(videoUri))
// }
// exoPlayer.setMediaSource(source)
// exoPlayer.prepare()
// }
//
//
// AndroidView(
//// modifier = Modifier.aspectRatio(video.width.toFloat() / video.height.toFloat()),
// factory = { context ->
// val frameLayout = FrameLayout(context)
//// frameLayout.setBackgroundColor(context.getColor(android.R.color.holo_blue_bright))
// frameLayout
// },
// update = { frameLayout ->
// frameLayout.removeAllViews()
//
// playerView = PlayerViewPool.get(frameLayout.context)
// PlayerView.switchTargetView(
// exoPlayer,
// PlayerViewPool.currentPlayerView,
// playerView
// )
// PlayerViewPool.currentPlayerView = playerView
// playerView!!.apply {
// player!!.playWhenReady = true
// }
//
// playerView?.apply {
// (parent as? ViewGroup)?.removeView(this)
// }
// frameLayout.addView(
// playerView,
// FrameLayout.LayoutParams.MATCH_PARENT,
// FrameLayout.LayoutParams.MATCH_PARENT
// )
////
//// playerView?.apply {
//// (parent as? ViewGroup)?.removeView(this)
//// PlayerViewPool.release(this)
//// }
//// playerView = null
//
// }
// )
//
// DisposableEffect(key1 = videoHelper.pickPlaybackResolution(video)) {
// onDispose {
// playerView?.apply {
// (parent as? ViewGroup)?.removeView(this)
// }
// exoPlayer.stop()
// playerView?.let {
// PlayerViewPool.release(it)
// }
// playerView = null
//
// }
// }
//
// }
//}
//
//

View File

@ -1,168 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.components
import androidx.compose.foundation.Image
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.material3.Icon
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import coil.annotation.ExperimentalCoilApi
import coil.compose.ImagePainter
import coil.compose.rememberImagePainter
import com.google.accompanist.placeholder.PlaceholderHighlight
import com.google.accompanist.placeholder.material.fade
import com.google.accompanist.placeholder.material.placeholder
import net.schueller.peertube.R
import net.schueller.peertube.common.Constants
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.presentation.common.getCreatorAvatar
import net.schueller.peertube.feature_video.presentation.common.getCreatorString
import net.schueller.peertube.feature_video.presentation.common.getMetaDataTag
import net.schueller.peertube.feature_video.presentation.video_play.VideoPlayEvent
import net.schueller.peertube.feature_video.presentation.video_play.VideoPlayViewModel
@ExperimentalCoilApi
@Composable
fun VideoMeta(
video: Video,
viewModel: VideoPlayViewModel = hiltViewModel()
) {
Column(
modifier = Modifier
.fillMaxSize()
.background(
color = MaterialTheme.colors.background
)
) {
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = {
viewModel.onEvent(VideoPlayEvent.OpenDescription(video))
},
)
) {
Text(
modifier = Modifier.padding(6.dp),
text = video.name ?: "",
// fontSize = 12.sp,
fontWeight = FontWeight.Bold,
style = MaterialTheme.typography.h6
)
Icon(
painterResource(id = R.drawable.ic_chevron_down),
modifier = Modifier
.size(32.dp)
.padding(end = 6.dp),
// .padding(end = 12.dp),
contentDescription = "Open Description"
)
}
Text(
modifier = Modifier.padding(start = 6.dp, end = 6.dp),
text = getMetaDataTag(video.createdAt, video.views, "",true),
fontWeight = FontWeight.Normal,
style = MaterialTheme.typography.caption
)
VideoActions(video)
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(5.dp)
.padding(
top = 2.dp,
bottom = 2.dp
)
.background(MaterialTheme.colors.onBackground)
)
Row(
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
// .padding(end = 12.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.height(56.dp),
) {
val avatar = rememberImagePainter(
data = Constants.BASE_IMAGE_URL + getCreatorAvatar(video)?.path
)
Image(
painter = avatar,
contentDescription = null,
modifier = Modifier
.height(48.dp)
.width(48.dp)
.padding(8.dp)
.clip(shape = CircleShape)
.placeholder(
visible = (avatar.state is ImagePainter.State.Error || avatar.state is ImagePainter.State.Empty),
highlight = if (avatar.state is ImagePainter.State.Empty) {
PlaceholderHighlight.fade()
} else {
null
},
),
// contentScale = ContentScale.Crop
)
Column(
// modifier = Modifier
// .padding(8.dp),
)
{
Text(
text = video.channel?.name ?: video.account?.name ?: "",
fontWeight = FontWeight.Normal,
style = MaterialTheme.typography.subtitle1
)
Text(
text = getCreatorString(video, true),
fontWeight = FontWeight.Normal,
style = MaterialTheme.typography.caption
)
}
}
Text(
modifier = Modifier
.padding(end = 16.dp),
text = "SUBSCRIBE",
fontWeight = FontWeight.Bold,
color = MaterialTheme.colors.primary,
style = MaterialTheme.typography.button
)
}
Spacer(
modifier = Modifier
.fillMaxWidth()
.height(5.dp)
.padding(
top = 2.dp,
bottom = 2.dp
)
.background(MaterialTheme.colors.onBackground)
)
}
}

View File

@ -1,66 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.components
import android.view.ViewGroup
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.viewinterop.AndroidView
import com.google.android.exoplayer2.ui.PlayerView
import net.schueller.peertube.feature_video.domain.model.Video
import net.schueller.peertube.feature_video.presentation.video_play.player.ExoPlayerHolder
import android.widget.FrameLayout
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.material.*
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.input.pointer.consumeAllChanges
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.dp
import net.schueller.peertube.feature_video.presentation.video_play.player.PlayerViewPool
@Composable
fun VideoScreen(
exoPlayerHolder: ExoPlayerHolder,
video: Video
) {
val context = LocalContext.current
val player = exoPlayerHolder.setVideo(video, context)
AndroidView(
modifier = Modifier
.fillMaxWidth(),
// modifier = Modifier.aspectRatio(video.width.toFloat() / video.height.toFloat()),
factory = { context ->
val frameLayout = FrameLayout(context)
// frameLayout.setBackgroundColor(context.getColor(android.R.color.holo_blue_bright))
frameLayout
},
update = { frameLayout ->
frameLayout.removeAllViews()
val playerView = PlayerViewPool.get(frameLayout.context)
playerView.player = player
PlayerView.switchTargetView(
player,
PlayerViewPool.currentPlayerView,
playerView
)
PlayerViewPool.currentPlayerView = playerView
frameLayout.addView(
playerView,
FrameLayout.LayoutParams.MATCH_PARENT,
FrameLayout.LayoutParams.MATCH_PARENT
)
}
)
}

View File

@ -1,27 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.player
import android.content.Context
import com.google.android.exoplayer2.database.StandaloneDatabaseProvider
import com.google.android.exoplayer2.upstream.cache.LeastRecentlyUsedCacheEvictor
import com.google.android.exoplayer2.upstream.cache.SimpleCache
object CacheHolder {
private var cache: SimpleCache? = null
private val lock = Object()
fun get(context: Context): SimpleCache {
synchronized(lock) {
if (cache == null) {
val cacheSize = 20L * 1024 * 1024
val exoDatabaseProvider = StandaloneDatabaseProvider(context)
cache = SimpleCache(
context.cacheDir,
LeastRecentlyUsedCacheEvictor(cacheSize),
exoDatabaseProvider
)
}
}
return cache!!
}
}

View File

@ -1,42 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.player
import android.content.Context
import com.google.android.exoplayer2.upstream.DataSource
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory
import com.google.android.exoplayer2.upstream.FileDataSource
import com.google.android.exoplayer2.upstream.cache.CacheDataSink
import com.google.android.exoplayer2.upstream.cache.CacheDataSource
import com.google.android.exoplayer2.util.Util
object DataSourceHolder {
private var cacheDataSourceFactory: CacheDataSource.Factory? = null
private var defaultDataSourceFactory: DataSource.Factory? = null
fun getCacheFactory(context: Context): CacheDataSource.Factory {
if (cacheDataSourceFactory == null) {
val simpleCache = CacheHolder.get(context)
val defaultFactory = getDefaultFactory(context)
cacheDataSourceFactory = CacheDataSource.Factory()
.setCache(simpleCache)
.setUpstreamDataSourceFactory(defaultFactory)
.setCacheReadDataSourceFactory(FileDataSource.Factory())
.setCacheWriteDataSinkFactory(
CacheDataSink.Factory()
.setCache(simpleCache)
.setFragmentSize(CacheDataSink.DEFAULT_FRAGMENT_SIZE)
)
}
return cacheDataSourceFactory!!
}
private fun getDefaultFactory(context: Context): DataSource.Factory {
if (defaultDataSourceFactory == null) {
defaultDataSourceFactory = DefaultDataSourceFactory(
context,
Util.getUserAgent(context, context.packageName)
)
}
return defaultDataSourceFactory!!
}
}

View File

@ -1,65 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.player
import android.content.Context
import android.net.Uri
import androidx.compose.runtime.LaunchedEffect
import com.google.android.exoplayer2.*
import com.google.android.exoplayer2.source.ProgressiveMediaSource
import com.google.android.exoplayer2.source.dash.DashMediaSource
import com.google.android.exoplayer2.source.hls.HlsMediaSource
import com.google.android.exoplayer2.util.Util
import net.schueller.peertube.common.VideoHelper
import net.schueller.peertube.feature_video.domain.model.Video
import javax.inject.Singleton
@Singleton
object ExoPlayerHolder {
private var exoplayer: ExoPlayer? = null
private var currentVideo: Video? = null
private val videoHelper = VideoHelper()
fun setVideo(video: Video, context: Context): ExoPlayer {
if (exoplayer == null) {
exoplayer = createExoPlayer(context)
}
// check if its the same video
if (currentVideo === null || currentVideo?.uuid !== video.uuid) {
val videoUri = Uri.parse(videoHelper.pickPlaybackResolution(video))
val dataSourceFactory = DataSourceHolder.getCacheFactory(context)
val source = when (Util.inferContentType(videoUri)) {
C.TYPE_DASH -> DashMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(videoUri))
C.TYPE_HLS -> HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(videoUri))
else -> ProgressiveMediaSource.Factory(dataSourceFactory)
.createMediaSource(MediaItem.fromUri(videoUri))
}
exoplayer!!.setMediaSource(source)
exoplayer!!.prepare()
exoplayer!!.playWhenReady = true
currentVideo = video
}
return exoplayer!!
}
private fun createExoPlayer(context: Context): ExoPlayer {
return ExoPlayer.Builder(context)
.setLoadControl(
DefaultLoadControl.Builder().setBufferDurationsMs(
DefaultLoadControl.DEFAULT_MIN_BUFFER_MS,
DefaultLoadControl.DEFAULT_MAX_BUFFER_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS / 10,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS / 10
).build()
)
.build()
.apply {
repeatMode = Player.REPEAT_MODE_ONE
}
}
}

View File

@ -1,25 +0,0 @@
package net.schueller.peertube.feature_video.presentation.video_play.player
import android.content.Context
import android.view.LayoutInflater
import androidx.core.util.Pools
import com.google.android.exoplayer2.ui.PlayerView
import net.schueller.peertube.R
object PlayerViewPool {
var currentPlayerView: PlayerView? = null
private val playerViewPool = Pools.SimplePool<PlayerView>(2)
fun get(context: Context): PlayerView {
return playerViewPool.acquire() ?: createPlayerView(context)
}
fun release(player: PlayerView) {
playerViewPool.release(player)
}
private fun createPlayerView(context: Context): PlayerView {
return (LayoutInflater.from(context).inflate(R.layout.exoplayer_texture_view, null, false) as PlayerView)
}
}

View File

@ -1,163 +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.Intent
import android.os.Bundle
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
import net.schueller.peertube.activity.SearchServerActivity
import net.schueller.peertube.database.Server
import net.schueller.peertube.database.ServerViewModel
import net.schueller.peertube.databinding.FragmentAddServerBinding
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.utils.hideKeyboard
class AddServerFragment : Fragment() {
private lateinit var mBinding: FragmentAddServerBinding
private val mServerViewModel: ServerViewModel by activityViewModels()
private var mServer: Server? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
arguments?.let {
mServer = it.getParcelable(SERVER_ARG)
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
mBinding = FragmentAddServerBinding.inflate(inflater, container, false)
return mBinding.root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initServerEdit()
mBinding.addServerButton.setOnClickListener {
var formValid = true
hideKeyboard()
if (mBinding.serverLabel.text.toString().isBlank()) {
mBinding.serverLabel.error = getString(R.string.server_book_label_is_required)
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_LONG).show()
formValid = false
}
// validate url
mBinding.serverUrl.apply {
APIUrlHelper.cleanServerUrl(text.toString())?.let {
setText(it)
if (!Patterns.WEB_URL.matcher(it).matches()) {
error = getString(R.string.server_book_valid_url_is_required)
Toast.makeText(context, R.string.invalid_url, Toast.LENGTH_LONG).show()
formValid = false
}
}
}
if (formValid) {
mServer?.apply {
mBinding.let {
serverName = it.serverLabel.text.toString()
serverHost = it.serverUrl.text.toString()
username = it.serverUsername.text.toString()
password = it.serverPassword.text.toString()
mServerViewModel.update(this)
}
return@setOnClickListener
}
mBinding.apply {
val server = Server(serverName = serverLabel.text.toString())
server.serverHost = serverUrl.text.toString()
server.username = serverUsername.text.toString()
server.password = serverPassword.text.toString()
mServerViewModel.insert(server)
}
}
}
mBinding.pickServerUrl.setOnClickListener {
val intentServer = Intent(activity, SearchServerActivity::class.java)
openActivityForResult(intentServer)
}
}
private fun initServerEdit() {
mServer?.let {
mBinding.apply {
serverLabel.setText(it.serverName)
serverUrl.setText(it.serverHost)
serverUsername.setText(it.username)
serverPassword.setText(it.password)
addServerButton.text = getString(R.string.server_book_add_save_button)
}
}
}
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 SERVER_ARG = "server"
fun newInstance(server: Server) = AddServerFragment().apply {
arguments = Bundle().also {
it.putParcelable(SERVER_ARG, server)
}
}
}
}

View File

@ -1,120 +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.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.ImageButton
import net.schueller.peertube.R
import android.widget.TextView
import androidx.fragment.app.Fragment
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.Description
import net.schueller.peertube.model.Video
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
class VideoDescriptionFragment : Fragment () {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val view = inflater.inflate(
R.layout.fragment_video_description, container,
false
)
val video = video
if (video != null) {
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val videoDataService = RetrofitInstance.getRetrofitInstance(
apiBaseURL,
APIUrlHelper.useInsecureConnection(context)
).create(
GetVideoDataService::class.java
)
// description, get extended if available
val videoDescription = view.findViewById<TextView>(R.id.description)
val shortDescription = video.description
if (shortDescription != null && shortDescription.length > 237) {
val call = videoDataService.getVideoFullDescription(video.uuid);
call.enqueue(object : Callback<Description?> {
override fun onResponse(call: Call<Description?>, response: Response<Description?>) {
val videoFullDescription: Description? = response.body();
videoDescription.text = videoFullDescription?.description
}
override fun onFailure(call: Call<Description?>, t: Throwable) {
Log.wtf(TAG, t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(activity, t)
}
})
}
videoDescription.text = shortDescription;
val closeButton = view.findViewById<ImageButton>(R.id.video_description_close_button)
closeButton.setOnClickListener {
videoMetaDataFragment!!.hideDescriptionFragment()
}
// video privacy
val videoPrivacy = view.findViewById<TextView>(R.id.video_privacy);
videoPrivacy.text = video!!.privacy.label;
// video category
val videoCategory = view.findViewById<TextView>(R.id.video_category);
videoCategory.text = video!!.category.label;
// video privacy
val videoLicense = view.findViewById<TextView>(R.id.video_license);
videoLicense.text = video!!.licence.label;
// video language
val videoLanguage = view.findViewById<TextView>(R.id.video_language);
videoLanguage.text = video!!.language.label;
// video privacy
val videoTags = view.findViewById<TextView>(R.id.video_tags);
videoTags.text = android.text.TextUtils.join(", ", video!!.tags);
}
return view
}
companion object {
private var video: Video? = null
private var videoMetaDataFragment: VideoMetaDataFragment? = null
const val TAG = "VideoDescr"
fun newInstance(mVideo: Video?, mVideoMetaDataFragment: VideoMetaDataFragment): VideoDescriptionFragment {
video = mVideo
videoMetaDataFragment = mVideoMetaDataFragment
return VideoDescriptionFragment()
}
}
}

View File

@ -1,127 +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.content.Context;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.mikepenz.iconics.Iconics;
import net.schueller.peertube.R;
import net.schueller.peertube.model.File;
import net.schueller.peertube.model.Resolution;
import java.util.ArrayList;
import androidx.annotation.Nullable;
public class VideoMenuQualityFragment extends BottomSheetDialogFragment {
private static ArrayList<File> mFiles;
public static final String TAG = "VideoMenuQuality";
private static File autoQualityFile;
public static VideoMenuQualityFragment newInstance(Context context, ArrayList<File> files) {
mFiles = files;
// Auto quality
if (autoQualityFile == null) {
autoQualityFile = new File();
Resolution autoQualityResolution = new Resolution();
autoQualityResolution.setId(999999);
autoQualityResolution.setLabel(context.getString(R.string.menu_video_options_quality_automated));
autoQualityFile.setId(999999);
autoQualityFile.setResolution(autoQualityResolution);
}
if (!mFiles.contains(autoQualityFile)) {
mFiles.add(0, autoQualityFile);
}
return new VideoMenuQualityFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_video_options_quality_popup_menu, container,
false);
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getContext());
Integer videoQuality = sharedPref.getInt(getString(R.string.pref_quality_key), 999999);
for (File file : mFiles) {
LinearLayout menuRow = (LinearLayout) inflater.inflate(R.layout.row_popup_menu, container);
TextView iconView = menuRow.findViewById(R.id.video_quality_icon);
iconView.setId(file.getResolution().getId());
TextView textView = menuRow.findViewById(R.id.video_quality_text);
Log.v(TAG, file.getResolution().getLabel());
textView.setText(file.getResolution().getLabel());
textView.setOnClickListener(view1 -> {
// Log.v(TAG, file.getResolution().getLabel());
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.pref_quality_key), file.getResolution().getId());
editor.apply();
for (File fileV : mFiles) {
TextView iconViewV = view.findViewById(fileV.getResolution().getId());
if (iconViewV != null) {
iconViewV.setText("");
}
}
iconView.setText(R.string.video_quality_active_icon);
new Iconics.Builder().on(iconView).build();
//TODO: set new video quality on running video
});
// Add to menu
LinearLayout menuHolder = view.findViewById(R.id.video_quality_menu);
menuHolder.addView(menuRow);
// Set current
if (videoQuality.equals(file.getResolution().getId())) {
iconView.setText(R.string.video_quality_active_icon);
new Iconics.Builder().on(iconView).build();
}
}
return view;
}
}

View File

@ -1,133 +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.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.mikepenz.iconics.Iconics;
import net.schueller.peertube.R;
import net.schueller.peertube.service.VideoPlayerService;
import androidx.annotation.Nullable;
public class VideoMenuSpeedFragment extends BottomSheetDialogFragment {
private static VideoPlayerService videoPlayerService;
public static final String TAG = "VideoMenuSpeed";
private TextView speed05Icon;
private TextView speed075Icon;
private TextView speed10Icon;
private TextView speed125Icon;
private TextView speed15Icon;
private TextView speed20Icon;
public static VideoMenuSpeedFragment newInstance(VideoPlayerService mService) {
videoPlayerService = mService;
return new VideoMenuSpeedFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_video_options_speed_popup_menu, container,
false);
// Icons
speed05Icon = view.findViewById(R.id.video_speed05_icon);
speed075Icon = view.findViewById(R.id.video_speed075_icon);
speed10Icon = view.findViewById(R.id.video_speed10_icon);
speed125Icon = view.findViewById(R.id.video_speed125_icon);
speed15Icon = view.findViewById(R.id.video_speed15_icon);
speed20Icon = view.findViewById(R.id.video_speed20_icon);
// Buttons
TextView speed05 = view.findViewById(R.id.video_speed05);
TextView speed075 = view.findViewById(R.id.video_speed075);
TextView speed10 = view.findViewById(R.id.video_speed10);
TextView speed125 = view.findViewById(R.id.video_speed125);
TextView speed15 = view.findViewById(R.id.video_speed15);
TextView speed20 = view.findViewById(R.id.video_speed20);
setDefaultVideoSpeed();
// Attach the listener
speed05.setOnClickListener(v -> setVideoSpeed(0.5f, speed05Icon));
speed075.setOnClickListener(v -> setVideoSpeed(0.75f, speed075Icon));
speed10.setOnClickListener(v -> setVideoSpeed(1.0f, speed10Icon));
speed125.setOnClickListener(v -> setVideoSpeed(1.25f, speed125Icon));
speed15.setOnClickListener(v -> setVideoSpeed(1.5f, speed15Icon));
speed20.setOnClickListener(v -> setVideoSpeed(2.0f, speed20Icon));
return view;
}
private void setVideoSpeed(Float speed, TextView icon) {
speed05Icon.setText("");
speed075Icon.setText("");
speed10Icon.setText("");
speed125Icon.setText("");
speed15Icon.setText("");
speed20Icon.setText("");
videoPlayerService.setPlayBackSpeed(speed);
icon.setText(R.string.video_speed_active_icon);
new Iconics.Builder().on(icon).build();
}
private void setDefaultVideoSpeed() {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getContext());
String speed = sharedPref.getString(getString(R.string.pref_video_speed_key), "1.0");
switch (speed) {
case "0.5":
setVideoSpeed(0.5f, speed05Icon);
break;
case "0.75":
setVideoSpeed(0.75f, speed075Icon);
break;
case "1.0":
setVideoSpeed(1.0f, speed10Icon);
break;
case "1.25":
setVideoSpeed(1.25f, speed125Icon);
break;
case "1.5":
setVideoSpeed(1.5f, speed15Icon);
break;
case "2.0":
setVideoSpeed(2.0f, speed20Icon);
break;
}
}
}

View File

@ -1,230 +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.Context
import android.content.res.ColorStateList
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.TextView
import androidx.fragment.app.Fragment
import androidx.fragment.app.activityViewModels
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.R
import net.schueller.peertube.adapter.MultiViewRecycleViewAdapter
import net.schueller.peertube.database.VideoViewModel
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.helper.ErrorHelper
import net.schueller.peertube.model.CommentThread
import net.schueller.peertube.model.Rating
import net.schueller.peertube.model.Video
import net.schueller.peertube.model.VideoList
import net.schueller.peertube.model.ui.VideoMetaViewItem
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import net.schueller.peertube.service.VideoPlayerService
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
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
private val mVideoViewModel: VideoViewModel by activityViewModels()
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)
}
})
}
fun saveToPlaylist(video: Video) {
val playlistVideo: net.schueller.peertube.database.Video = net.schueller.peertube.database.Video(videoUUID = video.uuid, videoName = video.name, videoDescription = video.description)
mVideoViewModel.insert(playlistVideo)
}
companion object {
const val TAG = "VMDF"
}
}

View File

@ -1,130 +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.content.SharedPreferences;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.LinearLayout;
import android.widget.TextView;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.mikepenz.iconics.Iconics;
import net.schueller.peertube.R;
import net.schueller.peertube.model.File;
import net.schueller.peertube.service.VideoPlayerService;
import java.util.ArrayList;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
public class VideoOptionsFragment extends BottomSheetDialogFragment {
private static VideoPlayerService videoPlayerService;
private static ArrayList<File> files;
public static final String TAG = "VideoOptions";
public static VideoOptionsFragment newInstance(VideoPlayerService mService, ArrayList<File> mFiles) {
videoPlayerService = mService;
files = mFiles;
return new VideoOptionsFragment();
}
@Nullable
@Override
public View onCreateView(LayoutInflater inflater,
@Nullable ViewGroup container,
@Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_video_options_popup_menu, container,
false);
LinearLayout menuHolder = view.findViewById(R.id.video_options_popup);
// Video Speed
LinearLayout menuRow = (LinearLayout) inflater.inflate(R.layout.row_popup_menu, container);
TextView iconView = menuRow.findViewById(R.id.video_quality_icon);
TextView textView = menuRow.findViewById(R.id.video_quality_text);
textView.setText(
getString(
R.string.menu_video_options_playback_speed,
getCurrentVideoPlaybackSpeedString(videoPlayerService.getPlayBackSpeed()
)
)
);
iconView.setText(R.string.video_option_speed_icon);
new Iconics.Builder().on(iconView).build();
textView.setOnClickListener(view1 -> {
VideoMenuSpeedFragment videoMenuSpeedFragment =
VideoMenuSpeedFragment.newInstance(videoPlayerService);
videoMenuSpeedFragment.show(requireActivity().getSupportFragmentManager(),
VideoMenuSpeedFragment.TAG);
});
menuHolder.addView(menuRow);
// Video Quality
LinearLayout menuRow2 = (LinearLayout) inflater.inflate(R.layout.row_popup_menu, container);
TextView iconView2 = menuRow2.findViewById(R.id.video_quality_icon);
TextView textView2 = menuRow2.findViewById(R.id.video_quality_text);
textView2.setText(String.format(getString(R.string.menu_video_options_quality), getCurrentVideoQuality(files)));
iconView2.setText(R.string.video_option_quality_icon);
new Iconics.Builder().on(iconView2).build();
textView2.setOnClickListener(view1 -> {
VideoMenuQualityFragment videoMenuQualityFragment =
VideoMenuQualityFragment.newInstance(getContext(), files);
videoMenuQualityFragment.show(requireActivity().getSupportFragmentManager(),
VideoMenuQualityFragment.TAG);
});
menuHolder.addView(menuRow2);
return view;
}
private String getCurrentVideoQuality(ArrayList<File> files) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(getContext());
Integer videoQuality = sharedPref.getInt(getString(R.string.pref_quality_key), 999999);
for (File file : files) {
if (videoQuality.equals(file.getResolution().getId())) {
return file.getResolution().getLabel();
}
}
// Returning Automated as a placeholder
return getString(R.string.menu_video_options_quality_automated);
}
private String getCurrentVideoPlaybackSpeedString(float playbackSpeed) {
String speed = String.valueOf(playbackSpeed);
// Remove all non-digit characters from the string
speed = speed.replaceAll("[^0-9]", "");
// Dynamically get the localized string corresponding to the speed
@StringRes int stringId = getResources().getIdentifier("video_speed_" + speed, "string", videoPlayerService.getPackageName());
return getString(stringId);
}
}

View File

@ -1,496 +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 com.google.android.exoplayer2.video.VideoRendererEventListener
import com.google.android.exoplayer2.ui.PlayerView
import android.content.Intent
import net.schueller.peertube.service.VideoPlayerService
import com.github.se_bastiaan.torrentstream.TorrentStream
import android.widget.LinearLayout
import android.view.GestureDetector
import android.content.ServiceConnection
import android.content.ComponentName
import android.os.IBinder
import net.schueller.peertube.service.VideoPlayerService.LocalBinder
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout.AspectRatioListener
import android.view.LayoutInflater
import android.view.ViewGroup
import android.os.Bundle
import net.schueller.peertube.R
import android.app.Activity
import android.app.PictureInPictureParams
import android.content.Context
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout
import android.widget.TextView
import android.widget.FrameLayout
import net.schueller.peertube.helper.APIUrlHelper
import net.schueller.peertube.network.GetVideoDataService
import net.schueller.peertube.network.RetrofitInstance
import android.widget.Toast
import net.schueller.peertube.helper.ErrorHelper
import android.content.pm.ActivityInfo
import android.net.Uri
import android.os.Build
import com.github.se_bastiaan.torrentstream.listeners.TorrentListener
import com.github.se_bastiaan.torrentstream.Torrent
import com.github.se_bastiaan.torrentstream.StreamStatus
import com.google.android.exoplayer2.decoder.DecoderCounters
import android.view.View.OnTouchListener
import android.view.MotionEvent
import android.view.GestureDetector.SimpleOnGestureListener
import androidx.annotation.RequiresApi
import android.os.Build.VERSION_CODES
import android.os.Environment
import android.util.Log
import android.view.View
import android.widget.ProgressBar
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.fragment.app.Fragment
import com.github.se_bastiaan.torrentstream.TorrentOptions.Builder
import com.google.android.exoplayer2.Format
import com.google.android.exoplayer2.util.Util
import com.mikepenz.iconics.Iconics
import net.schueller.peertube.R.layout
import net.schueller.peertube.R.string
import net.schueller.peertube.helper.VideoHelper
import net.schueller.peertube.model.Video
import retrofit2.Call
import retrofit2.Callback
import retrofit2.Response
import java.lang.Exception
import kotlin.math.abs
class VideoPlayerFragment : Fragment(), VideoRendererEventListener {
var videoUuid: String? = null
private set
// private var progressBar: ProgressBar? = null
private var exoPlayer: PlayerView? = null
private var videoPlayerIntent: Intent? = null
private var mBound = false
private var isFullscreen = false
private var mService: VideoPlayerService? = null
private var torrentStream: TorrentStream? = null
// private var torrentStatus: LinearLayout? = null
var videoAspectRatio = 0f
private set
private var mDetector: GestureDetector? = null
private val mConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(className: ComponentName, service: IBinder) {
Log.d(TAG, "onServiceConnected")
val binder = service as LocalBinder
mService = binder.service
// 2. Create the player
exoPlayer!!.player = mService!!.player
mBound = true
loadVideo()
}
override fun onServiceDisconnected(componentName: ComponentName) {
Log.d(TAG, "onServiceDisconnected")
exoPlayer!!.player = null
mBound = false
}
}
private val aspectRatioListener: AspectRatioListener = AspectRatioListener {
targetAspectRatio, _, _ -> videoAspectRatio = targetAspectRatio
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
// Inflate the layout for this fragment
return inflater.inflate(layout.fragment_video_player, container, false)
}
fun start(videoUuid: String?) {
// start service
val context = context
val activity: Activity? = activity
this.videoUuid = videoUuid
assert(activity != null)
// progressBar = activity?.findViewById(R.id.torrent_progress)
// progressBar?.max = 100
assert(context != null)
exoPlayer = PlayerView(context!!)
exoPlayer = activity?.findViewById(R.id.video_view)
exoPlayer?.controllerShowTimeoutMs = 1000
exoPlayer?.resizeMode = AspectRatioFrameLayout.RESIZE_MODE_FIT
mDetector = GestureDetector(context, MyGestureListener())
exoPlayer?.setOnTouchListener(touchListener)
exoPlayer?.setAspectRatioListener(aspectRatioListener)
// torrentStatus = activity?.findViewById(R.id.exo_torrent_status)
// Full screen Icon
val fullscreenText = activity?.findViewById<TextView>(R.id.exo_fullscreen)
val fullscreenButton = activity?.findViewById<FrameLayout>(R.id.exo_fullscreen_button)
fullscreenText?.setText(string.video_expand_icon)
if (fullscreenText != null) {
Iconics.Builder().on(fullscreenText).build()
fullscreenButton?.setOnClickListener {
Log.d(TAG, "Fullscreen")
fullScreenToggle()
}
}
if (!mBound) {
videoPlayerIntent = Intent(context, VideoPlayerService::class.java)
activity?.bindService(videoPlayerIntent, mConnection, Context.BIND_AUTO_CREATE)
}
}
private fun loadVideo() {
val context = context
// get video details from api
val apiBaseURL = APIUrlHelper.getUrlWithVersion(context)
val service =
RetrofitInstance.getRetrofitInstance(apiBaseURL, APIUrlHelper.useInsecureConnection(context)).create(
GetVideoDataService::class.java
)
val call = service.getVideoData(videoUuid)
call.enqueue(object : Callback<Video?> {
override fun onResponse(call: Call<Video?>, response: Response<Video?>) {
val video = response.body()
mService!!.setCurrentVideo(video)
if (video == null) {
Toast.makeText(
context,
"Unable to retrieve video information, try again later.",
Toast.LENGTH_SHORT
).show()
return
}
playVideo(video)
}
override fun onFailure(call: Call<Video?>, t: Throwable) {
Log.wtf(TAG, t.fillInStackTrace())
ErrorHelper.showToastFromCommunicationError(activity, t)
}
})
}
fun useController(value: Boolean) {
if (mBound) {
exoPlayer!!.useController = value
}
}
private fun playVideo(video: Video) {
val context = context
// video Meta fragment
val videoMetaDataFragment =
(requireActivity().supportFragmentManager.findFragmentById(R.id.video_meta_data_fragment) as VideoMetaDataFragment?)!!
videoMetaDataFragment.updateVideoMeta(video, mService)
val sharedPref = context?.getSharedPreferences(
context.packageName + "_preferences",
Context.MODE_PRIVATE
)
var prefTorrentPlayer = false
var videoQuality = 999999
if (sharedPref != null) {
prefTorrentPlayer = sharedPref.getBoolean(getString(string.pref_torrent_player_key), false)
videoQuality = sharedPref.getInt(getString(string.pref_quality_key), 999999)
}
// if (prefTorrentPlayer) {
// torrentStatus!!.visibility = View.VISIBLE
// val stream = video.files[0].torrentUrl
// Log.v(TAG, "getTorrentUrl : " + video.files[0].torrentUrl)
// torrentStream = setupTorrentStream()
// torrentStream!!.startStream(stream)
// } else {
var urlToPlay: String? = null
var isHLS = false
// try HLS stream first
// get video qualities
// TODO: if auto is set all versions except 0p should be added to a track and have exoplayer auto select optimal bitrate
if (video.streamingPlaylists.size > 0) {
urlToPlay = video.streamingPlaylists[0].playlistUrl
isHLS = true
} else {
if (video.files.size > 0) {
urlToPlay = video.files[0].fileUrl // default, take first found, usually highest res
for (file in video.files) {
// Set quality if it matches
if (file.resolution.id == videoQuality) {
urlToPlay = file.fileUrl
}
}
}
}
if (urlToPlay!!.isNotEmpty()) {
mService!!.setCurrentStreamUrl(urlToPlay, isHLS)
// torrentStatus!!.visibility = View.GONE
startPlayer()
} else {
stopVideo()
Toast.makeText(context, string.api_error, Toast.LENGTH_LONG).show()
}
// }
Log.v(TAG, "end of load Video")
}
private fun startPlayer() {
Util.startForegroundService(requireContext(), videoPlayerIntent!!)
}
fun destroyVideo() {
exoPlayer!!.player = null
if (torrentStream != null) {
torrentStream!!.stopStream()
}
}
fun pauseVideo() {
if (mBound) {
mService!!.player!!.playWhenReady = false
}
}
// fun pauseToggle() {
// if (mBound) {
// mService!!.player!!.playWhenReady = !mService!!.player!!.playWhenReady
// }
// }
fun unPauseVideo() {
if (mBound) {
mService!!.player!!.playWhenReady = true
}
}
val isPaused: Boolean
get() = !mService!!.player!!.playWhenReady
fun showControls(value: Boolean) {
exoPlayer!!.useController = value
}
fun stopVideo() {
if (mBound) {
requireContext().unbindService(mConnection)
mBound = false
}
}
/**
* triggered rotation and button press
*/
fun setIsFullscreen(fullscreen: Boolean) {
isFullscreen = fullscreen
val fullscreenButton = requireActivity().findViewById<TextView>(R.id.exo_fullscreen)
if (fullscreen) {
hideSystemBars()
fullscreenButton.setText(string.video_compress_icon)
} else {
restoreSystemBars()
fullscreenButton.setText(string.video_expand_icon)
}
Iconics.Builder().on(fullscreenButton).build()
}
private fun hideSystemBars()
{
val view = this.view
if (view != null) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(view) ?: return
// Configure the behavior of the hidden system bars
windowInsetsController.systemBarsBehavior =
WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
windowInsetsController.hide(WindowInsetsCompat.Type.systemBars())
}
}
private fun restoreSystemBars()
{
val view = this.view
if (view != null) {
val windowInsetsController =
ViewCompat.getWindowInsetsController(view) ?: return
// Show both the status bar and the navigation bar
windowInsetsController.show(WindowInsetsCompat.Type.systemBars())
}
}
fun getIsFullscreen(): Boolean {
return isFullscreen
}
/**
* Triggered by button press
*/
fun fullScreenToggle() {
if (!isFullscreen) {
setIsFullscreen(true)
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} else {
setIsFullscreen(false)
// we want to force portrait if fullscreen is switched of as we do not have a min. landscape view
requireActivity().requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
}
//
// /**
// * Torrent Playback
// *
// * @return torrent stream
// */
// private fun setupTorrentStream(): TorrentStream {
// val torrentOptions = Builder()
// .saveLocation(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS))
// .removeFilesAfterStop(true)
// .build()
// val torrentStream = TorrentStream.init(torrentOptions)
// torrentStream.addListener(object : TorrentListener {
// override fun onStreamReady(torrent: Torrent) {
// val videoPath = Uri.fromFile(torrent.videoFile).toString()
// Log.d(TAG, "Ready! torrentStream videoPath:$videoPath")
// mService!!.setCurrentStreamUrl(videoPath, false)
// startPlayer()
// }
//
// override fun onStreamProgress(torrent: Torrent, streamStatus: StreamStatus) {
// if (streamStatus.bufferProgress <= 100 && progressBar!!.progress < 100 && progressBar!!.progress != streamStatus.bufferProgress) {
// //Log.d(TAG, "Progress: " + streamStatus.bufferProgress);
// progressBar!!.progress = streamStatus.bufferProgress
// }
// }
//
// override fun onStreamStopped() {
// Log.d(TAG, "Stopped")
// }
//
// override fun onStreamPrepared(torrent: Torrent) {
// Log.d(TAG, "Prepared")
// }
//
// override fun onStreamStarted(torrent: Torrent) {
// Log.d(TAG, "Started")
// }
//
// override fun onStreamError(torrent: Torrent, e: Exception) {
// Log.d(TAG, "Error: " + e.message)
// }
// })
// return torrentStream
// }
override fun onVideoEnabled(counters: DecoderCounters) {
Log.v(TAG, "onVideoEnabled()...")
}
override fun onVideoDecoderInitialized(
decoderName: String,
initializedTimestampMs: Long,
initializationDurationMs: Long
) {
}
override fun onVideoInputFormatChanged(format: Format) {}
override fun onDroppedFrames(count: Int, elapsedMs: Long) {}
override fun onVideoDisabled(counters: DecoderCounters) {
Log.v(TAG, "onVideoDisabled()...")
}
// touch event on video player
private var touchListener = OnTouchListener { _, event ->
//v.performClick() // causes flicker but should be implemented for accessibility
mDetector!!.onTouchEvent(event)
}
internal inner class MyGestureListener : SimpleOnGestureListener() {
/*
@Override
public boolean onDown(MotionEvent event) {
Log.d("TAG","onDown: ");
return true;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
Log.i("TAG", "onSingleTapConfirmed: ");
pauseToggle();
return true;
}
@Override
public void onLongPress(MotionEvent e) {
Log.i("TAG", "onLongPress: ");
}
@Override
public boolean onDoubleTap(MotionEvent e) {
Log.i("TAG", "onDoubleTap: ");
return true;
}
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2,
float distanceX, float distanceY) {
Log.i("TAG", "onScroll: ");
return true;
}
*/
@RequiresApi(api = VERSION_CODES.N)
override fun onFling(
event1: MotionEvent, event2: MotionEvent,
velocityX: Float, velocityY: Float
): Boolean {
Log.d(TAG, event1.toString())
Log.d(TAG, event2.toString())
Log.d(TAG, velocityX.toString())
Log.d(TAG, velocityY.toString())
//arbitrarily velocity speeds that seem to work to differentiate events.
if (velocityY > 4000) {
Log.d(TAG, "we have a drag down event")
if (VideoHelper.canEnterPipMode(context)) {
if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
val pipParams = PictureInPictureParams.Builder()
requireActivity().enterPictureInPictureMode(pipParams.build())
}
}
}
if (velocityX > 2000 && abs(velocityY) < 2000) {
Log.d(TAG, "swipe right $velocityY")
}
if (velocityX < 2000 && abs(velocityY) < 2000) {
Log.d(TAG, "swipe left $velocityY")
}
return true
}
}
companion object {
private const val TAG = "VideoPlayerFragment"
}
}

View File

@ -1,74 +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.helper;
import android.content.Context;
import android.content.SharedPreferences;
import android.preference.PreferenceManager;
import android.util.Patterns;
import android.webkit.URLUtil;
import android.widget.Toast;
import net.schueller.peertube.R;
public class APIUrlHelper{
public static String getUrl(Context context) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
// validate URL is valid
String URL = sharedPref.getString(context.getString(R.string.pref_api_base_key), context.getResources().getString(R.string.pref_default_api_base_url));
if (!URLUtil.isValidUrl(URL)) {
return "http://invalid";
}
return URL;
}
public static String getUrlWithVersion(Context context) {
return APIUrlHelper.getUrl(context) + "/api/v1/";
}
public static Boolean useInsecureConnection(Context context) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
return sharedPref.getBoolean(context.getString(R.string.pref_accept_insecure), false);
}
public static String getShareUrl(Context context, String videoUuid) {
return APIUrlHelper.getUrl(context) + "/videos/watch/" + videoUuid;
}
public static String getServerIndexUrl(Context context) {
return "https://instances.joinpeertube.org/api/v1/";
}
public static String cleanServerUrl(String url) {
String cleanUrl = url.toLowerCase();
cleanUrl = cleanUrl.replace(" ", "");
if (!cleanUrl.startsWith("http")) {
cleanUrl = "https://" + cleanUrl;
}
if (cleanUrl.endsWith("/")) {
cleanUrl = cleanUrl.substring(0, cleanUrl.length() - 1);
}
return cleanUrl;
}
}

View File

@ -1,42 +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.helper;
import android.content.Context;
import android.widget.Toast;
import net.schueller.peertube.R;
import java.io.IOException;
import retrofit2.HttpException;
public class ErrorHelper {
public static void showToastFromCommunicationError( Context context, Throwable throwable ) {
if (throwable instanceof IOException ) {
//handle network error
Toast.makeText( context, context.getString( R.string.network_error), Toast.LENGTH_SHORT).show();
} else if (throwable instanceof HttpException ) {
//handle HTTP error response code
Toast.makeText(context, context.getString(R.string.api_error), Toast.LENGTH_SHORT).show();
} else {
//handle other exceptions
Toast.makeText(context, context.getString(R.string.api_error), Toast.LENGTH_SHORT).show();
}
}
}

View File

@ -1,111 +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.helper
import android.content.Context
import android.text.format.DateUtils
import net.schueller.peertube.R
import net.schueller.peertube.R.string
import net.schueller.peertube.model.Account
import net.schueller.peertube.model.Avatar
import net.schueller.peertube.model.Video
import org.ocpsoft.prettytime.PrettyTime
import java.util.Date
import java.util.Locale
import kotlin.math.absoluteValue
object MetaDataHelper {
@JvmStatic
fun getMetaString(getCreatedAt: Date, viewCount: Int, context: Context, reversed: Boolean = false): String {
// Compatible with SDK 21+
val currentLanguage = Locale.getDefault().displayLanguage
val p = PrettyTime(currentLanguage)
val relativeTime = p.format(Date(getCreatedAt.time))
return if (reversed) {
viewCount.toString() +
context.resources.getString(string.meta_data_views) +
context.resources.getString(string.meta_data_seperator) +
relativeTime
} else {
relativeTime +
context.resources.getString(string.meta_data_seperator) +
viewCount + context.resources.getString(string.meta_data_views)
}
}
fun getTagsString(video: Video): String {
return if (video.tags.isNotEmpty()) {
" #" + video.tags.joinToString(" #", "", "", 3, "")
} else {
" "
}
}
@JvmStatic
fun getCreatorString(video: Video, context: Context, fqdn: Boolean = false): String {
return if (isChannel(video)) {
if (!fqdn) {
video.channel.displayName
} else {
getConcatFqdnString(video.channel.name, video.channel.host, context)
}
} else {
getOwnerString(video.account, context, fqdn)
}
}
@JvmStatic
fun getOwnerString(account: Account, context: Context, fqdn: Boolean = true): String {
return if (!fqdn) {
account.name
} else {
getConcatFqdnString(account.name, account.host, context)
}
}
private fun getConcatFqdnString(user: String, host: String, context: Context): String {
return context.resources.getString(string.video_owner_fqdn_line, user, host)
}
@JvmStatic
fun getCreatorAvatar(video: Video, context: Context): Avatar? {
return if (isChannel(video)) {
if (video.channel.avatar == null) {
video.account.avatar
} else {
video.channel.avatar
}
} else {
video.account.avatar
}
}
@JvmStatic
fun isChannel(video: Video): Boolean {
// c285b523-d688-43c5-a9ad-f745ff09bbd1
return !video.channel.name.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}".toRegex())
}
@JvmStatic
fun getDuration(duration: Long?): String {
return DateUtils.formatElapsedTime(duration!!)
}
}

View File

@ -1,56 +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.helper;
import android.app.AppOpsManager;
import android.content.Context;
import android.content.SharedPreferences;
import android.os.Build;
import android.preference.PreferenceManager;
import android.util.Log;
import net.schueller.peertube.R;
public class VideoHelper {
private static final String TAG = "VideoHelper";
public static boolean canEnterPipMode(Context context) {
SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(context);
context.getString(R.string.pref_background_float_key);
// pref is disabled
if (!context.getString(R.string.pref_background_float_key).equals(
sharedPref.getString(
context.getString(R.string.pref_background_behavior_key),
context.getString(R.string.pref_background_float_key))
)
) {
return false;
}
// api does not support it
Log.v(TAG, "api version " + Build.VERSION.SDK_INT);
if (Build.VERSION.SDK_INT > 27) {
AppOpsManager appOpsManager = (AppOpsManager) context.getSystemService(Context.APP_OPS_SERVICE);
return (AppOpsManager.MODE_ALLOWED == appOpsManager.checkOp(AppOpsManager.OPSTR_PICTURE_IN_PICTURE, android.os.Process.myUid(), context.getPackageName()));
}
return false;
}
}

View File

@ -1,146 +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 net.schueller.peertube.helper.APIUrlHelper
import android.webkit.MimeTypeMap
import android.os.Environment
import android.webkit.URLUtil
import android.widget.Toast
import androidx.core.app.ActivityCompat
import net.schueller.peertube.R
import net.schueller.peertube.model.Video
import android.content.ContextWrapper
import android.app.Activity
object Intents {
private const val TAG = "Intents"
/**
* https://troll.tv/videos/watch/6edbd9d1-e3c5-4a6c-8491-646e2020469c
*
* @param context context
* @param video video
*/
// TODO, offer which version to download
@JvmStatic
fun Share(context: Context, video: Video) {
val intent = Intent()
intent.action = Intent.ACTION_SEND
intent.putExtra(Intent.EXTRA_SUBJECT, video.name)
intent.putExtra(Intent.EXTRA_TEXT, APIUrlHelper.getShareUrl(context, video.uuid))
intent.type = "text/plain"
context.startActivity(intent)
}
/**
* @param context context
* @param video video
*/
// TODO, offer which version to download
fun Download(context: Context, video: Video) {
// deal withe permissions here
// get permission to store file
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) != PackageManager.PERMISSION_GRANTED
) {
val activity = getActivity(context)
if (activity != null) {
ActivityCompat.requestPermissions(
activity,
arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
0
)
}
if (ActivityCompat.checkSelfPermission(
context,
Manifest.permission.WRITE_EXTERNAL_STORAGE
) == PackageManager.PERMISSION_GRANTED
) {
startDownload(video, context)
} else {
Toast.makeText(
context,
context.getString(R.string.video_download_permission_error),
Toast.LENGTH_LONG
).show()
}
} else {
startDownload(video, context)
}
}
private fun startDownload(video: Video, context: Context)
{
if (video.files.size > 0) {
val url = video.files[0].fileDownloadUrl
// make sure it is a valid filename
val destFilename = video.name.replace(
"[^a-zA-Z0-9]".toRegex(),
"_"
) + "." + MimeTypeMap.getFileExtensionFromUrl(
URLUtil.guessFileName(url, null, null)
)
//Toast.makeText(context, destFilename, Toast.LENGTH_LONG).show();
val request = DownloadManager.Request(Uri.parse(url))
request.setDescription(video.description)
request.setTitle(video.name)
request.allowScanningByMediaScanner()
request.setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE_NOTIFY_COMPLETED)
request.setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, destFilename)
// get download service and enqueue file
val manager = context.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
manager.enqueue(request)
} else {
Toast.makeText(context, R.string.api_error, Toast.LENGTH_LONG).show()
}
}
private fun getActivity(context: Context?): Activity? {
if (context == null) {
return null
} else if (context is ContextWrapper) {
return if (context is Activity) {
context
} else {
getActivity(context.baseContext)
}
}
return null
}
}

View File

@ -1,34 +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.model
import java.util.Date
class Account(
var id: Int,
var url: String,
var uuid: String,
var name: String,
var host: String,
var followingCount: Int,
var followersCount: Int,
var avatar: Avatar?,
var createdAt: Date,
var updatedAt: Date,
var displayName: String,
var description: String
)

View File

@ -1,25 +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.model
import java.util.Date
class Avatar(
var path: String,
var createdAt: Date,
var updatedAt: Date
)

View File

@ -1,25 +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.model
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
class Category: OverviewRecycleViewItem() {
var id: Int? = null
var label: String? = null
}

View File

@ -1,25 +0,0 @@
/*
* Copyright 2018 Stefan Schüller <sschueller@techdroid.com>
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import java.util.ArrayList
class CategoryVideo(
val category: Category,
val videos: ArrayList<Video>
)

View File

@ -1,37 +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.model
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import java.util.Date
class Channel(
val id: Int,
val url: String,
val uuid: String,
val name: String,
val host: String,
val followingCount: Int,
val followersCount: Int,
val avatar: Avatar?,
val createdAt: Date,
val updatedAt: Date,
val displayName: String,
val description: String,
val support: String,
val local: Boolean
): OverviewRecycleViewItem()

View File

@ -1,32 +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.model;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
public class ChannelList {
@SerializedName("data")
private ArrayList<Channel> channelList;
public ArrayList<Channel> getChannelArrayList() {
return channelList;
}
}

View File

@ -1,41 +0,0 @@
/*
* Copyright 2018 Stefan Schüller <sschueller@techdroid.com>
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model;
import java.util.ArrayList;
public class ChannelVideo {
private Channel channel;
private ArrayList<Video> videos;
public Channel getChannel() {
return channel;
}
public void setChannel(Channel channel) {
this.channel = channel;
}
public ArrayList<Video> getVideos() {
return videos;
}
public void setVideos(ArrayList<Video> videos) {
this.videos = videos;
}
}

View File

@ -1,38 +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.model
import java.util.*
class Comment(
val id: Int,
val url: String,
val text: String,
val threadId: Int,
val inReplyToCommentId: Int? = null,
val videoId: Int,
val createdAt: Date,
val updatedAt: Date,
val deletedAt: Date? = null,
val isDeleted: Boolean,
val totalRepliesFromVideoAuthor: Int,
val totalReplies: Int,
val account: Account
)

View File

@ -1,29 +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.model
import com.google.gson.annotations.SerializedName
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import java.util.ArrayList
class CommentThread(
val total: Int,
val totalNotDeletedComments: Int,
@SerializedName("data")
val comments: ArrayList<Comment>
): OverviewRecycleViewItem()

View File

@ -1,32 +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.model;
public class Config {
// TODO: implement remaining items
private String serverVersion;
public String getServerVersion() {
return serverVersion;
}
public void setServerVersion(String serverVersion) {
this.serverVersion = serverVersion;
}
}

View File

@ -1,30 +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.model;
public class Description {
private String description;
public String getDescription() {
return description;
}
public void setDescription(final String description) {
this.description = description;
}
}

View File

@ -1,110 +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.model;
public class File {
private Integer id;
private String fileDownloadUrl;
private Integer fps;
private Resolution resolution;
private String resolutionLabel;
private String magnetUri;
private Integer size;
private String torrentUrl;
private String torrentDownloadUrl;
private String fileUrl;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getFileDownloadUrl() {
return fileDownloadUrl;
}
public void setFileDownloadUrl(String fileDownloadUrl) {
this.fileDownloadUrl = fileDownloadUrl;
}
public Integer getFps() {
return fps;
}
public void setFps(Integer fps) {
this.fps = fps;
}
public Resolution getResolution() {
return resolution;
}
public void setResolution(Resolution resolution) {
this.resolution = resolution;
}
public String getResolutionLabel() {
return resolutionLabel;
}
public void setResolutionLabel(String resolutionLabel) {
this.resolutionLabel = resolutionLabel;
}
public String getMagnetUri() {
return magnetUri;
}
public void setMagnetUri(String magnetUri) {
this.magnetUri = magnetUri;
}
public Integer getSize() {
return size;
}
public void setSize(Integer size) {
this.size = size;
}
public String getTorrentUrl() {
return torrentUrl;
}
public void setTorrentUrl(String torrentUrl) {
this.torrentUrl = torrentUrl;
}
public String getTorrentDownloadUrl() {
return torrentDownloadUrl;
}
public void setTorrentDownloadUrl(String torrentDownloadUrl) {
this.torrentDownloadUrl = torrentDownloadUrl;
}
public String getFileUrl() {
return fileUrl;
}
public void setFileUrl(String fileUrl) {
this.fileUrl = fileUrl;
}
}

View File

@ -1,39 +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.model;
public class Language {
private String id;
private String label;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View File

@ -1,39 +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.model;
public class Licence {
private Integer id;
private String label;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View File

@ -1,160 +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.model;
public class Me {
private Integer id;
private Account account;
private Boolean autoPlayVideo;
private Boolean blocked;
private String blockedReason;
private String createdAt;
private String email;
private String emailVerified;
private String nsfwPolicy;
private Integer role;
private String roleLabel;
private String username;
// private VideoChannels videoChannels;
private Integer videoQuota;
private Integer videoQuotaDaily;
private String webTorrentEnabled;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public Account getAccount() {
return account;
}
public void setAccount(Account account) {
this.account = account;
}
public Boolean getAutoPlayVideo() {
return autoPlayVideo;
}
public void setAutoPlayVideo(Boolean autoPlayVideo) {
this.autoPlayVideo = autoPlayVideo;
}
public Boolean getBlocked() {
return blocked;
}
public void setBlocked(Boolean blocked) {
this.blocked = blocked;
}
public String getBlockedReason() {
return blockedReason;
}
public void setBlockedReason(String blockedReason) {
this.blockedReason = blockedReason;
}
public String getCreatedAt() {
return createdAt;
}
public void setCreatedAt(String createdAt) {
this.createdAt = createdAt;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getEmailVerified() {
return emailVerified;
}
public void setEmailVerified(String emailVerified) {
this.emailVerified = emailVerified;
}
public String getNsfwPolicy() {
return nsfwPolicy;
}
public void setNsfwPolicy(String nsfwPolicy) {
this.nsfwPolicy = nsfwPolicy;
}
public Integer getRole() {
return role;
}
public void setRole(Integer role) {
this.role = role;
}
public String getRoleLabel() {
return roleLabel;
}
public void setRoleLabel(String roleLabel) {
this.roleLabel = roleLabel;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public Integer getVideoQuota() {
return videoQuota;
}
public void setVideoQuota(Integer videoQuota) {
this.videoQuota = videoQuota;
}
public Integer getVideoQuotaDaily() {
return videoQuotaDaily;
}
public void setVideoQuotaDaily(Integer videoQuotaDaily) {
this.videoQuotaDaily = videoQuotaDaily;
}
public String getWebTorrentEnabled() {
return webTorrentEnabled;
}
public void setWebTorrentEnabled(String webTorrentEnabled) {
this.webTorrentEnabled = webTorrentEnabled;
}
}

View File

@ -1,44 +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.model;
import com.google.gson.annotations.SerializedName;
public class OauthClient {
@SerializedName("client_id")
private String clientId;
@SerializedName("client_secret")
private String clientSecret;
public String getClientId() {
return clientId;
}
public void setClientId(String clientId) {
this.clientId = clientId;
}
public String getClientSecret() {
return clientSecret;
}
public void setClientSecret(String clientSecret) {
this.clientSecret = clientSecret;
}
}

View File

@ -1,26 +0,0 @@
/*
* Copyright 2018 Stefan Schüller <sschueller@techdroid.com>
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import java.util.ArrayList
class Overview(
val categories: ArrayList<CategoryVideo>,
val channels: ArrayList<ChannelVideo>,
val tags: ArrayList<TagVideo>
)

View File

@ -1,39 +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.model;
public class Privacy {
private Integer id;
private String label;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View File

@ -1,44 +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.model;
import com.google.gson.annotations.SerializedName;
public class Rating {
@SerializedName("videoId")
private Integer videoId;
@SerializedName("rating")
private String rating;
public Integer getVideoId() {
return videoId;
}
public void setVideoId(Integer videoId) {
this.videoId = videoId;
}
public String getRating() {
return rating;
}
public void setRating(String rating) {
this.rating = rating;
}
}

View File

@ -1,30 +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.model;
public class Redundancy {
private String baseUrl;
public String getBaseUrl() {
return baseUrl;
}
public void setBaseUrl(final String baseUrl) {
this.baseUrl = baseUrl;
}
}

View File

@ -1,39 +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.model;
public class Resolution {
private Integer id;
private String label;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View File

@ -1,213 +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.model;
import java.util.ArrayList;
import java.util.Date;
public class Server {
private Integer id;
private String host;
private String name;
private String shortDescription;
private String version;
private Boolean signupAllowed;
private Double userVideoQuota;
private Category category;
private ArrayList<String> languages;
private Boolean autoBlacklistUserVideosEnabled;
private String defaultNSFWPolicy;
private Boolean isNSFW;
private Integer totalUsers;
private Integer totalVideos;
private Integer totalLocalVideos;
private Integer totalInstanceFollowers;
private Integer totalInstanceFollowing;
private Boolean supportsIPv6;
private String country;
private Integer health;
private Date createdAt;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getShortDescription() {
return shortDescription;
}
public void setShortDescription(String shortDescription) {
this.shortDescription = shortDescription;
}
public String getVersion() {
return version;
}
public void setVersion(String version) {
this.version = version;
}
public Boolean getSignupAllowed() {
return signupAllowed;
}
public void setSignupAllowed(Boolean signupAllowed) {
this.signupAllowed = signupAllowed;
}
public Double getUserVideoQuota() {
return userVideoQuota;
}
public void setUserVideoQuota(Double userVideoQuota) {
this.userVideoQuota = userVideoQuota;
}
public Category getCategory() {
return category;
}
public void setCategory(Category category) {
this.category = category;
}
public ArrayList<String> getLanguages() {
return languages;
}
public void setLanguages(ArrayList<String> languages) {
this.languages = languages;
}
public Boolean getAutoBlacklistUserVideosEnabled() {
return autoBlacklistUserVideosEnabled;
}
public void setAutoBlacklistUserVideosEnabled(Boolean autoBlacklistUserVideosEnabled) {
this.autoBlacklistUserVideosEnabled = autoBlacklistUserVideosEnabled;
}
public String getDefaultNSFWPolicy() {
return defaultNSFWPolicy;
}
public void setDefaultNSFWPolicy(String defaultNSFWPolicy) {
this.defaultNSFWPolicy = defaultNSFWPolicy;
}
public Boolean getNSFW() {
return isNSFW;
}
public void setNSFW(Boolean NSFW) {
isNSFW = NSFW;
}
public Integer getTotalUsers() {
return totalUsers;
}
public void setTotalUsers(Integer totalUsers) {
this.totalUsers = totalUsers;
}
public Integer getTotalVideos() {
return totalVideos;
}
public void setTotalVideos(Integer totalVideos) {
this.totalVideos = totalVideos;
}
public Integer getTotalLocalVideos() {
return totalLocalVideos;
}
public void setTotalLocalVideos(Integer totalLocalVideos) {
this.totalLocalVideos = totalLocalVideos;
}
public Integer getTotalInstanceFollowers() {
return totalInstanceFollowers;
}
public void setTotalInstanceFollowers(Integer totalInstanceFollowers) {
this.totalInstanceFollowers = totalInstanceFollowers;
}
public Integer getTotalInstanceFollowing() {
return totalInstanceFollowing;
}
public void setTotalInstanceFollowing(Integer totalInstanceFollowing) {
this.totalInstanceFollowing = totalInstanceFollowing;
}
public Boolean getSupportsIPv6() {
return supportsIPv6;
}
public void setSupportsIPv6(Boolean supportsIPv6) {
this.supportsIPv6 = supportsIPv6;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public Integer getHealth() {
return health;
}
public void setHealth(Integer health) {
this.health = health;
}
public Date getCreatedAt() {
return createdAt;
}
public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}
}

View File

@ -1,32 +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.model;
import com.google.gson.annotations.SerializedName;
import java.util.ArrayList;
public class ServerList {
@SerializedName("data")
private ArrayList<Server> serverList;
public ArrayList<Server> getServerArrayList() {
return serverList;
}
}

View File

@ -1,45 +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.model;
public class State {
public static final int PUBLISHED = 1;
public static final int TO_TRANSCODE = 2;
public static final int TO_IMPORT = 3;
public static final int WAITING_FOR_LIVE = 4;
public static final int LIVE_ENDED = 5;
private Integer id;
private String label;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getLabel() {
return label;
}
public void setLabel(String label) {
this.label = label;
}
}

View File

@ -1,77 +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.model;
import java.util.ArrayList;
public class StreamingPlaylist {
private Integer id;
private Integer type;
private String playlistUrl;
private String segmentsSha256Url;
private ArrayList<Redundancy> redundancies;
private ArrayList<File> files;
public Integer getId() {
return id;
}
public void setId(final Integer id) {
this.id = id;
}
public Integer getType() {
return type;
}
public void setType(final Integer type) {
this.type = type;
}
public String getPlaylistUrl() {
return playlistUrl;
}
public void setPlaylistUrl(final String playlistUrl) {
this.playlistUrl = playlistUrl;
}
public String getSegmentsSha256Url() {
return segmentsSha256Url;
}
public void setSegmentsSha256Url(final String segmentsSha256Url) {
this.segmentsSha256Url = segmentsSha256Url;
}
public ArrayList<Redundancy> getRedundancies() {
return redundancies;
}
public void setRedundancies(final ArrayList<Redundancy> redundancies) {
this.redundancies = redundancies;
}
public ArrayList<File> getFiles() {
return files;
}
public void setFiles(final ArrayList<File> files) {
this.files = files;
}
}

View File

@ -1,26 +0,0 @@
/*
* Copyright 2018 Stefan Schüller <sschueller@techdroid.com>
*
* License: GPL-3.0+
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package net.schueller.peertube.model
import net.schueller.peertube.model.ui.OverviewRecycleViewItem
import java.util.ArrayList
class TagVideo(
var tag: String,
var videos: ArrayList<Video>
): OverviewRecycleViewItem()

View File

@ -1,66 +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.model;
import com.google.gson.annotations.SerializedName;
public class Token {
@SerializedName("access_token")
private String accessToken;
@SerializedName("expires_in")
private String expiresIn;
@SerializedName("refresh_token")
private String refreshToken;
@SerializedName("token_type")
private String tokenType;
public String getAccessToken() {
return accessToken;
}
public void setAccessToken(String accessToken) {
this.accessToken = accessToken;
}
public String getExpiresIn() {
return expiresIn;
}
public void setExpiresIn(String expiresIn) {
this.expiresIn = expiresIn;
}
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
public String getTokenType() {
return tokenType;
}
public void setTokenType(String tokenType) {
this.tokenType = tokenType;
}
}

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