diff --git a/CHANGELOG.md b/CHANGELOG.md index 0269056..a6a0853 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### Version 1.0.28 Tag: v1.0.28 (2019-03-03) +* Server selection +* Lots of translations + +### Version 1.0.27 Tag: v1.0.27 (2019-02-17) +* App Rename +* Lots of translations + ### Version 1.0.26 Tag: v1.0.26 (2019-01-27) * Larger fullscreen button target * Fixed duration colors diff --git a/README.md b/README.md index 5dcb6e2..567e070 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,67 @@

- Thorium, a Android PeerTube Client + Thorium, an Android PeerTube Client

-[![Latest release](https://img.shields.io/github/release/sschueller/peertube-android.svg)](https://github.com/sschueller/peertube-android/releases/latest) -[![F-Droid](https://img.shields.io/f-droid/v/net.schueller.peertube.svg)](https://f-droid.org/de/packages/net.schueller.peertube/) - -

-

-

- screenshot - screenshot + + + + + +

+## Screenshots +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png) +[](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png) + +## Description +PeerTube is a federated video streaming platform that is community-owned and ad-free, with no vendor lock-in. This client allows you to watch and browse videos on a server of your choice in the PeerTube network. + +This client comes preconfigured with one PeerTube server managed by the application creator - not the PeerTube project itself, which lists more on http://instances.joinpeertube.org/ - to allow you to have a taste of what the client is capable of. Choose your server to tune your experience! + +Please note this is app is in beta and is still missing a lot of features. + + ## Download * Beta Test on Google Play: https://play.google.com/store/apps/details?id=net.schueller.peertube * F-Droid: https://f-droid.org/packages/net.schueller.peertube/ +## Help Translate +* https://hosted.weblate.org/projects/peertube/ + ## Features -- [X] Recent Videos -- [X] Trending Videos -- [X] Endless scrolling -- [X] Pull to refresh -- [X] Very Basic Torrent playback -- [X] Change Server -- [X] Search -- [X] Themes / Dark mode -- [X] Background playback -- [X] NSFW Filter option -- [X] Authentication / Login -- [X] Like/dislike video -- [X] Video speed selection -- [X] Video quality selection +* Recent Videos +* Trending Videos +* Endless scrolling +* Pull to refresh +* Very Basic Torrent playback +* Change Server +* Search +* Themes / Dark mode +* Background playback +* NSFW Filter option +* Authentication / Login +* Like/dislike video +* Video speed selection +* Video quality selection +* Server selection -## TODO +## Coming soon -- [ ] Video Playback via WebRTC -- [ ] Video overlay play and draggable video window -- [ ] Comment videos -- [ ] Report Videos -- [ ] User / Channel Overview Page -- [ ] Unit Tests -- [ ] Lots more missing at this point... +* Video Playback via WebRTC +* Video overlay play and draggable video window +* Comment videos +* Report Videos +* User / Channel Overview Page +* Unit Tests +* Lots more... +## Contribution +Whether you have ideas, translations, design changes, code cleaning, or real heavy code changes, help is always welcome. +## Donate + + + \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index fb012d1..71bfb1b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,8 +6,8 @@ android { applicationId "net.schueller.peertube" minSdkVersion 21 targetSdkVersion 28 - versionCode 1027 - versionName "1.0.27" + versionCode 1028 + versionName "1.0.28" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ext { libVersions = [ @@ -78,4 +78,6 @@ android { dependencies { implementation 'com.android.support.constraint:constraint-layout:+' + implementation 'androidx.appcompat:appcompat:1.0.0-beta01' + implementation 'androidx.constraintlayout:constraintlayout:1.1.2' } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 073b0ba..5250d9f 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -2,73 +2,65 @@ - - - - - - - + + - - + tools:ignore="GoogleAppIndexingWarning"> + + + - + android:resource="@xml/searchable"> + android:label="@string/title_activity_login" + android:theme="@style/AppTheme.NoActionBar" /> + android:launchMode="singleTop" + android:theme="@style/AppTheme.NoActionBar" /> - - - - + + - - - - + android:theme="@style/AppTheme.NoActionBar" /> + - - \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/activity/SelectServerActivity.java b/app/src/main/java/net/schueller/peertube/activity/SelectServerActivity.java index 4a8789e..d29a8fa 100644 --- a/app/src/main/java/net/schueller/peertube/activity/SelectServerActivity.java +++ b/app/src/main/java/net/schueller/peertube/activity/SelectServerActivity.java @@ -1,61 +1,177 @@ -/* - * Copyright 2018 Stefan Schüller - * - * 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 . - */ - package net.schueller.peertube.activity; import androidx.annotation.NonNull; import androidx.appcompat.app.AppCompatActivity; -import android.os.Bundle; - -import net.schueller.peertube.R; -import net.schueller.peertube.model.ServerList; -import net.schueller.peertube.network.GetServerListDataService; -import net.schueller.peertube.network.RetrofitInstance; - +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.content.SharedPreferences; +import android.os.Bundle; +import android.preference.PreferenceManager; +import android.util.Log; +import android.view.View; +import android.widget.Button; +import android.widget.TextView; +import android.widget.Toast; + +import net.schueller.peertube.R; +import net.schueller.peertube.adapter.ServerAdapter; +import net.schueller.peertube.adapter.VideoAdapter; +import net.schueller.peertube.helper.APIUrlHelper; +import net.schueller.peertube.model.ServerList; +import net.schueller.peertube.model.VideoList; +import net.schueller.peertube.network.GetServerListDataService; +import net.schueller.peertube.network.GetVideoDataService; +import net.schueller.peertube.network.RetrofitInstance; + +import java.util.ArrayList; + +import static net.schueller.peertube.helper.Constants.DEFAULT_THEME; +import static net.schueller.peertube.helper.Constants.THEME_PREF_KEY; + public class SelectServerActivity extends AppCompatActivity { + private ServerAdapter serverAdapter; + private SwipeRefreshLayout swipeRefreshLayout; + + private int currentStart = 0; + private int count = 12; + + private TextView emptyView; + private RecyclerView recyclerView; + + private boolean isLoading = false; + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - setContentView(R.layout.activity_select_server); + setContentView(R.layout.activity_server_selection); - // get list of peertube servers + loadList(); - // TODO: Get here via settings, get data from API, add to adapter and show in recycle view, upon selection fill settings field + // set url + TextView selectedUrl = findViewById(R.id.serverSelectedUrl); + selectedUrl.setText(APIUrlHelper.getUrl(SelectServerActivity.this)); - GetServerListDataService service = RetrofitInstance.getRetrofitInstance("https://instances.joinpeertube.org/api/v1/").create(GetServerListDataService.class); - Call call = service.getInstancesData(0, 500); - call.enqueue(new Callback() { + Button setServerButton = findViewById(R.id.server_selection_set); + setServerButton.setOnClickListener(v -> { + + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(this); + SharedPreferences.Editor editor = sharedPref.edit(); + + String serverUrl = APIUrlHelper.cleanServerUrl(selectedUrl.getText().toString()); + + editor.putString("pref_api_base", serverUrl); + editor.apply(); + + this.finish(); + }); + + } + + + private void loadList() { + + recyclerView = findViewById(R.id.serverRecyclerView); + swipeRefreshLayout = findViewById(R.id.serversSwipeRefreshLayout); + + emptyView = findViewById(R.id.empty_server_selection_view); + + RecyclerView.LayoutManager layoutManager = new LinearLayoutManager(SelectServerActivity.this); + recyclerView.setLayoutManager(layoutManager); + + serverAdapter = new ServerAdapter(new ArrayList<>(), this); + recyclerView.setAdapter(serverAdapter); + + loadServers(currentStart, count); + + recyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - // response.body().getVideoArrayList(); + public void onScrollStateChanged(@NonNull RecyclerView recyclerView, int newState) { + super.onScrollStateChanged(recyclerView, newState); } @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { + 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); + } + } + } + + } + }); + + swipeRefreshLayout.setOnRefreshListener(() -> { + // Refresh items + if (!isLoading) { + currentStart = 0; + loadServers(currentStart, count); } }); } + + + + private void loadServers(int start, int count) { + isLoading = true; + + GetServerListDataService service = RetrofitInstance.getRetrofitInstance( + APIUrlHelper.getServerIndexUrl(SelectServerActivity.this) + ).create(GetServerListDataService.class); + + + Call call; + + call = service.getInstancesData(start, count); + + Log.d("URL Called", call.request().url() + ""); + + call.enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response 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 call, @NonNull Throwable t) { + Log.wtf("err", t.fillInStackTrace()); + Toast.makeText(SelectServerActivity.this, getString(R.string.api_error), Toast.LENGTH_SHORT).show(); + isLoading = false; + swipeRefreshLayout.setRefreshing(false); + } + }); + + } } diff --git a/app/src/main/java/net/schueller/peertube/activity/SettingsActivity.java b/app/src/main/java/net/schueller/peertube/activity/SettingsActivity.java index 3d7352d..6f79d93 100644 --- a/app/src/main/java/net/schueller/peertube/activity/SettingsActivity.java +++ b/app/src/main/java/net/schueller/peertube/activity/SettingsActivity.java @@ -26,14 +26,18 @@ import android.content.res.Configuration; import android.os.Build; import android.os.Bundle; import android.preference.Preference; + import androidx.appcompat.app.ActionBar; + import android.preference.PreferenceFragment; import android.preference.PreferenceManager; +import android.preference.PreferenceScreen; import android.util.Patterns; import android.view.MenuItem; import android.widget.Toast; import net.schueller.peertube.R; + import java.util.List; import static net.schueller.peertube.helper.Constants.DEFAULT_THEME; @@ -53,14 +57,14 @@ public class SettingsActivity extends AppCompatPreferenceActivity { return super.onOptionsItemSelected(item); } - private static String getSelectedColor(Context context, String colorId){ + private static String getSelectedColor(Context context, String colorId) { String res = "Color not found"; - String [ ] themeArray = context.getResources().getStringArray(R.array.themeValues); - String [ ] colorArray = context.getResources().getStringArray(R.array.themeArray); + String[] themeArray = context.getResources().getStringArray(R.array.themeValues); + String[] colorArray = context.getResources().getStringArray(R.array.themeArray); - for (int i = 0 ; i < themeArray.length ; i++){ - if (themeArray[i].equals(colorId)){ + for (int i = 0; i < themeArray.length; i++) { + if (themeArray[i].equals(colorId)) { res = colorArray[i]; break; } @@ -76,12 +80,12 @@ public class SettingsActivity extends AppCompatPreferenceActivity { String stringValue = value.toString(); // check URL is valid - if (preference.getKey().equals("pref_api_base") && !Patterns.WEB_URL.matcher(stringValue).matches()) { - Toast.makeText(preference.getContext(), R.string.invalid_url, Toast.LENGTH_LONG).show(); - return false; - } +// if (preference.getKey().equals("pref_api_base") && !Patterns.WEB_URL.matcher(stringValue).matches()) { +// Toast.makeText(preference.getContext(), R.string.invalid_url, Toast.LENGTH_LONG).show(); +// return false; +// } // Check if Theme color has change & Provide selected color - else if (preference.getKey().equals("pref_theme")) { + if (preference.getKey().equals("pref_theme")) { stringValue = getSelectedColor(preference.getContext(), stringValue); @@ -212,6 +216,27 @@ public class SettingsActivity extends AppCompatPreferenceActivity { } return super.onOptionsItemSelected(item); } + + @Override + public boolean onPreferenceTreeClick(PreferenceScreen preferenceScreen, + Preference preference) { + String key = preference.getKey(); + if (key.equals("pref_api_base")) { + Intent intentServer = new Intent(preference.getContext(), SelectServerActivity.class); + startActivity(intentServer); + return true; + } + return false; + } + + @Override + public void onResume() { + setPreferenceScreen(null); + addPreferencesFromResource(R.xml.pref_general); + bindPreferenceSummaryToValue(findPreference("pref_api_base")); + + super.onResume(); + } } } \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/activity/VideoListActivity.java b/app/src/main/java/net/schueller/peertube/activity/VideoListActivity.java index f851b73..8a68ce6 100644 --- a/app/src/main/java/net/schueller/peertube/activity/VideoListActivity.java +++ b/app/src/main/java/net/schueller/peertube/activity/VideoListActivity.java @@ -128,6 +128,9 @@ public class VideoListActivity extends CommonActivity { menu.findItem(R.id.action_account).setIcon( new IconicsDrawable(this, FontAwesome.Icon.faw_user_circle).actionBar()); +// menu.findItem(R.id.action_server_selection).setIcon( +// new IconicsDrawable(this, FontAwesome.Icon.faw_server).actionBar()); + MenuItem searchMenuItem = menu.findItem(R.id.action_search); searchMenuItem.setIcon( @@ -200,6 +203,10 @@ public class VideoListActivity extends CommonActivity { this.startActivity(intentMe); } return false; +// case R.id.action_server_selection: +// Intent intentServer = new Intent(this, SelectServerActivity.class); +// this.startActivity(intentServer); +// return false; default: break; } diff --git a/app/src/main/java/net/schueller/peertube/adapter/ServerAdapter.java b/app/src/main/java/net/schueller/peertube/adapter/ServerAdapter.java new file mode 100644 index 0000000..0e3de08 --- /dev/null +++ b/app/src/main/java/net/schueller/peertube/adapter/ServerAdapter.java @@ -0,0 +1,146 @@ +/* + * Copyright 2018 Stefan Schüller + * + * 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 . + */ +package net.schueller.peertube.adapter; + +import android.content.Context; +import android.content.SharedPreferences; +import android.preference.PreferenceManager; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.TextView; +import android.widget.Toast; + + +import net.schueller.peertube.R; +import net.schueller.peertube.activity.SelectServerActivity; +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; + + +public class ServerAdapter extends RecyclerView.Adapter { + + + private ArrayList serverList; + private SelectServerActivity activity; + private String baseUrl; + + public ServerAdapter(ArrayList serverList, SelectServerActivity 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_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.shortDescription.setText(serverList.get(position).getShortDescription()); + + holder.itemView.setOnClickListener(v -> { + + SharedPreferences sharedPref = PreferenceManager.getDefaultSharedPreferences(activity); + SharedPreferences.Editor editor = sharedPref.edit(); + + String serverUrl = APIUrlHelper.cleanServerUrl(serverList.get(position).getHost()); + + editor.putString("pref_api_base", serverUrl); + editor.apply(); + + activity.finish(); + + Toast.makeText(activity, activity.getString(R.string.server_selection_set_server, serverUrl), Toast.LENGTH_LONG).show(); + + }); + +// +// +// holder.moreButton.setText(R.string.video_more_icon); +// new Iconics.IconicsBuilder().ctx(context).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 data) { + serverList.addAll(data); + this.notifyDataSetChanged(); + } + + public void clearData() { + serverList.clear(); + this.notifyDataSetChanged(); + } + + @Override + public int getItemCount() { + return serverList.size(); + } + + class AccountViewHolder extends RecyclerView.ViewHolder { + + TextView name, host, signupAllowed, shortDescription; + + AccountViewHolder(View itemView) { + super(itemView); + name = itemView.findViewById(R.id.name); + host = itemView.findViewById(R.id.host); + signupAllowed = itemView.findViewById(R.id.signupAllowed); + shortDescription = itemView.findViewById(R.id.shortDescription); + + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/net/schueller/peertube/helper/APIUrlHelper.java b/app/src/main/java/net/schueller/peertube/helper/APIUrlHelper.java index 4a4c06a..3cc5a00 100644 --- a/app/src/main/java/net/schueller/peertube/helper/APIUrlHelper.java +++ b/app/src/main/java/net/schueller/peertube/helper/APIUrlHelper.java @@ -44,4 +44,23 @@ public class APIUrlHelper{ 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(); + + if (!cleanUrl.startsWith("http")) { + cleanUrl = "https://" + cleanUrl; + } + + if (cleanUrl.endsWith("/")) { + cleanUrl = cleanUrl.substring(0, cleanUrl.length() - 1); + } + + return cleanUrl; + } } diff --git a/app/src/main/java/net/schueller/peertube/model/Server.java b/app/src/main/java/net/schueller/peertube/model/Server.java index 4d14404..2cc17e1 100644 --- a/app/src/main/java/net/schueller/peertube/model/Server.java +++ b/app/src/main/java/net/schueller/peertube/model/Server.java @@ -25,12 +25,16 @@ public class Server { private String shortDescription; private String version; private Boolean signupAllowed; - private Integer userVideoQuota; + private Double userVideoQuota; private Integer totalUsers; private Integer totalVideos; private Integer totalLocalVideos; private Integer totalInstanceFollowers; private Integer totalInstanceFollowing; + + private Boolean supportsIPv6; + private String country; + private Integer health; public Integer getId() { @@ -81,11 +85,11 @@ public class Server { this.signupAllowed = signupAllowed; } - public Integer getUserVideoQuota() { + public Double getUserVideoQuota() { return userVideoQuota; } - public void setUserVideoQuota(Integer userVideoQuota) { + public void setUserVideoQuota(Double userVideoQuota) { this.userVideoQuota = userVideoQuota; } @@ -129,6 +133,22 @@ public class Server { 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; } diff --git a/app/src/main/res/layout/activity_select_server.xml b/app/src/main/res/layout/activity_select_server.xml deleted file mode 100644 index 71beae8..0000000 --- a/app/src/main/res/layout/activity_select_server.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/activity_server_selection.xml b/app/src/main/res/layout/activity_server_selection.xml new file mode 100644 index 0000000..0abd720 --- /dev/null +++ b/app/src/main/res/layout/activity_server_selection.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + +