diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3ba13e0ce..5a97b3662 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1 +1,8 @@ blank_issues_enabled: false +contact_links: + - name: 💬 IRC + url: https://webchat.freenode.net/#newpipe + about: Chat with us via IRC for quick Q/A + - name: 💬 Matrix + url: https://matrix.to/#/#freenode_#newpipe:matrix.org + about: Chat with us via Matrix for quick Q/A diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f8960bdc..a3ea00e03 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,6 +1,13 @@ name: CI -on: [push, pull_request] +on: + pull_request: + branches: + - dev + push: + branches: + - dev + - master jobs: build-and-test: @@ -33,3 +40,34 @@ jobs: with: name: app path: app/build/outputs/apk/debug/*.apk +# sonar: +# runs-on: ubuntu-latest +# steps: +# - uses: actions/checkout@v2 +# with: +# fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis + +# - name: Set up JDK 11 +# uses: actions/setup-java@v1.4.3 +# with: +# java-version: 11 # Sonar requires JDK 11 + +# - name: Cache SonarCloud packages +# uses: actions/cache@v2 +# with: +# path: ~/.sonar/cache +# key: ${{ runner.os }}-sonar +# restore-keys: ${{ runner.os }}-sonar + +# - name: Cache Gradle packages +# uses: actions/cache@v2 +# with: +# path: ~/.gradle/caches +# key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} +# restore-keys: ${{ runner.os }}-gradle + +# - name: Build and analyze +# env: +# GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any +# SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} +# run: ./gradlew build sonarqube --info diff --git a/README.es.md b/README.es.md new file mode 100644 index 000000000..0aa198d2c --- /dev/null +++ b/README.es.md @@ -0,0 +1,140 @@ +
+Capturas de pantalla • Descripción • Características • Installación y actualizaciones • Contribución • Donar • Licencias
+Sitio web • Blog • Preguntas Frecuentes • Prensa
+
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_01.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_02.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_03.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_04.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_05.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_06.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_07.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_08.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_09.png)
+[
](fastlane/metadata/android/en-US/images/phoneScreenshots/shot_10.png)
+[
](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_11.png)
+[
](fastlane/metadata/android/en-US/images/tenInchScreenshots/shot_12.png)
+
+## Descripción
+NewPipe no usa ninguna librería de framework de Google, ni la API de YouTube. Los sitios web solamente se analizan para extraer la información requerida, asi que esta app se puede usar sin los servicios de Google instalados. Además, no se necesita una cuenta de YouTube para usar NewPipe, lo cual es un software libre de copyleft.
+
+### Características
+* Buscar videos
+* Mostrar información general sobre videos
+* Mirar videos de YouTube
+* Escuchar audio de YouTube
+* Modo popup (reproductor flotante)
+* Elegir reproductor para mirar el video
+* Descargar videos
+* Descargar solamente audio
+* Abrir video en Kodi
+* Mostrar videos próximos/relacionados
+* Buscar a través de YouTube en un idioma específico
+* Mirar/Bloquear materiales restringidas por edad.
+* Mostrar información general sobre canales
+* Buscar canales
+* Mirar videos de un canal
+* Apoyo Orbot/Tor (todavía no directamente)
+* Apoyo 1080p/2K/4K
+* Ver historias
+* Subscribirse a canales
+* Buscar historias
+* Buscar/mirar listas de reproducción
+* Mirar listas de reproducción en fila
+* Poner videos en fila
+* Listas locales de reproducción
+* Subtítulos
+* Apoyo de medios en directo
+* Mostrar comentarios
+
+### Servicios apoyados
+NewPipe apoya varios servicios. Nuestras [documentaciones](https://teamnewpipe.github.io/documentation/) proveen más información en como se puede agregar un servicio nuevo a la app y el extractor. Por favor contáctenos si pretende agregar uno nuevo. Actualmente los servicios apoyados son:
+
+* YouTube
+* SoundCloud \[beta\]
+* media.ccc.de \[beta\]
+* PeerTube instances \[beta\]
+* Bandcamp \[beta\]
+
+
+
+
+## Installación y actualizaciones
+Se puede instalar NewPipe usando uno de los métodos siguientes:
+ 1. Agregar nuestro repositorio personalizado a F-Droid e instalarlo desde allí. Las instrucciones están aquí: https://newpipe.schabi.org/FAQ/tutorials/install-add-fdroid-repo/
+ 2. Descargar el archivo APK del enlace [Github Releases](https://github.com/TeamNewPipe/NewPipe/releases) e instalarlo.
+ 3. Actualizar a través de F-Droid. Este es el método más lento para obtener la actualización, como F-Droid debe reconocer cambios, construir el APK aparte, firmarlo con una clave, y finalmente empujar la actualización a los usuarios.
+ 4. Construir un APK de depuración por si mismo. Este es el modo más rápido para realizar nuevas características en su dispositivo, pero es mucho más complicado, asi que recomendamos uno de los otros métodos.
+
+Recomendamos el método 1 para la mayoría de usuarios. Los APKs instalados usando método 1 o 2 son compatibles el uno con el otro, pero no con las instalaciones usando método 3. Esta es debida a la misma clave digital (la nuestra), siendo utilizado en los métodos 1 y 2, pero una clave digital diferente (la de F-Droid) siendo utilizado en el método 3. Construir un APK de depuración usando método 4 excluye una clave enteramente. Firmando con claves digitales ayuda a asegurar de que un
+usuario no esté engañado para instalar una actualización maliciosa a una app.
+
+Mientras tanto, si quiere cambiar los fuentes por alguna razón (por ejemplo, la funcionalidad del nucleo de NewPipe se rompe y F-Droid aun no tiene la actualización), recomendamos el siguiente procedimiento:
+1. Repaldear sus datos a través de Ajustes > Contenido > Exporta base de datos para guardar su historia, subscripciones, y listas de reproducción
+2. Desinstalar NewPipe
+3. Descargar el APK del nuevo fuente e instalarlo.
+4. Importar los datos del paso 1 a través de Ajustes > Contenido > Importa base de datos.
+
+## Contribución
+Si tiene ideas, traducciónes, cambios de diseño, limpieza de código, o cambios grandes de código, su ayuda es siempre bienvenida.
+Cuanto más realizamos, mejor se pone la aplicación!
+
+Si quiere involucrarse, fíjese en nuestras [notas de contribución](.github/CONTRIBUTING.md).
+
+
+
+
+
+## Donar
+Si le gusta el NewPipe estaremos felices con una donación. O puede enviar bitcoin o donar a través de Bountysource o Liberapay. Para obtener más información sobre como donar a NewPipe, por favor visita nuestro [sitio web](https://newpipe.net/donate).
+
+![]() |
+ 16A9J59ahMRqkLSZjhYj33n9j3fMztFxnh | +|
![]() |
+ ||
![]() |
+
Website-ka • Maqaalada • Su'aalaha Aalaa La-iswaydiiyo • Warbaahinta
\n");
}
// add the logs
- for (int i = 0; i < errorList.length; i++) {
+ for (int i = 0; i < errorInfo.getStackTraces().length; i++) {
htmlErrorReport.append(" \n")
- .append("\n```\n").append(errorList[i]).append("\n```\n")
+ .append("\n```\n").append(errorInfo.getStackTraces()[i]).append("\n```\n")
.append("Crash log ");
- if (errorList.length > 1) {
+ if (errorInfo.getStackTraces().length > 1) {
htmlErrorReport.append(i + 1);
}
htmlErrorReport.append("")
.append("
@@ -82,6 +81,10 @@ public class KioskFragment extends BaseListInfoFragment
+ * This is necessary when the thumbnail's height is larger than the device's height
+ * and thus is enlarging the player's height
+ * causing the bottom playback controls to be out of the visible screen.
+ *
+ * The calculating follows these rules:
+ *
+ * It will return true if the device 's theme is dark, false otherwise.
+ *
+ * From https://developer.android.com/guide/topics/ui/look-and-feel/darktheme#java
+ *
+ * @param context the context to use
+ * @return true:dark theme, false:light or unknown
+ */
+ public static boolean isDeviceDarkThemeEnabled(final Context context) {
+ final int deviceTheme = context.getResources().getConfiguration().uiMode
+ & Configuration.UI_MODE_NIGHT_MASK;
+ switch (deviceTheme) {
+ case Configuration.UI_MODE_NIGHT_YES:
+ return true;
+ case Configuration.UI_MODE_NIGHT_UNDEFINED:
+ case Configuration.UI_MODE_NIGHT_NO:
+ default:
+ return false;
+ }
+ }
}
diff --git a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
index bea4b6f94..41a254b49 100644
--- a/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
+++ b/app/src/main/java/us/shandian/giga/ui/adapter/MissionAdapter.java
@@ -41,9 +41,9 @@ import com.google.android.material.snackbar.Snackbar;
import org.schabi.newpipe.BuildConfig;
import org.schabi.newpipe.R;
import org.schabi.newpipe.extractor.NewPipe;
-import org.schabi.newpipe.report.ErrorActivity;
-import org.schabi.newpipe.report.ErrorInfo;
-import org.schabi.newpipe.report.UserAction;
+import org.schabi.newpipe.error.ErrorActivity;
+import org.schabi.newpipe.error.ErrorInfo;
+import org.schabi.newpipe.error.UserAction;
import org.schabi.newpipe.util.NavigationHelper;
import org.schabi.newpipe.util.ShareUtils;
@@ -583,16 +583,12 @@ public class MissionAdapter extends Adapter> flowable = historyRecordManager
.getRelatedSearches(query, 3, 25);
@@ -763,8 +749,8 @@ public class SearchFragment extends BaseListFragment
disposables.add(deleteReactor
.observeOn(AndroidSchedulers.mainThread())
- .subscribe(ignored -> { /*Do nothing on success*/ }, this::onError))
- )
+ .subscribe(ignored -> { /*Do nothing on success*/ }, throwable ->
+ showError(new ErrorInfo(throwable,
+ UserAction.REQUESTED_BOOKMARK,
+ "Deleting playlist")))))
.setNegativeButton(R.string.cancel, null)
.show();
}
@@ -314,7 +307,10 @@ public final class BookmarkFragment extends BaseLocalListFragment
{ /*Do nothing on success*/ }, this::onError);
+ .subscribe(longs -> { /*Do nothing on success*/ }, throwable -> showError(
+ new ErrorInfo(throwable,
+ UserAction.REQUESTED_BOOKMARK,
+ "Changing playlist name")));
disposables.add(disposable);
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
index 04090abc6..1df999144 100644
--- a/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
+++ b/app/src/main/java/org/schabi/newpipe/local/feed/FeedFragment.kt
@@ -38,17 +38,18 @@ import icepick.State
import org.schabi.newpipe.R
import org.schabi.newpipe.database.feed.model.FeedGroupEntity
import org.schabi.newpipe.databinding.FragmentFeedBinding
+import org.schabi.newpipe.error.ErrorInfo
+import org.schabi.newpipe.error.UserAction
import org.schabi.newpipe.fragments.list.BaseListFragment
import org.schabi.newpipe.ktx.animate
+import org.schabi.newpipe.ktx.animateHideRecyclerViewAllowingScrolling
import org.schabi.newpipe.local.feed.service.FeedLoadService
-import org.schabi.newpipe.report.UserAction
import org.schabi.newpipe.util.Localization
-import java.util.Calendar
+import java.time.OffsetDateTime
class FeedFragment : BaseListFragment
removeWatchedStreams(false))
- .setNeutralButton(
- R.string.remove_watched_popup_yes_and_partially_watched_videos,
- (DialogInterface d, int id) -> removeWatchedStreams(true))
- .setNegativeButton(R.string.cancel,
- (DialogInterface d, int id) -> d.cancel())
- .create()
- .show();
- }
- break;
- default:
- return super.onOptionsItemSelected(item);
+ if (item.getItemId() == R.id.menu_item_remove_watched) {
+ if (!isRemovingWatched) {
+ new AlertDialog.Builder(requireContext())
+ .setMessage(R.string.remove_watched_popup_warning)
+ .setTitle(R.string.remove_watched_popup_title)
+ .setPositiveButton(R.string.yes,
+ (DialogInterface d, int id) -> removeWatchedStreams(false))
+ .setNeutralButton(
+ R.string.remove_watched_popup_yes_and_partially_watched_videos,
+ (DialogInterface d, int id) -> removeWatchedStreams(true))
+ .setNegativeButton(R.string.cancel,
+ (DialogInterface d, int id) -> d.cancel())
+ .create()
+ .show();
+ }
+ } else {
+ return super.onOptionsItemSelected(item);
}
return true;
}
@@ -455,7 +455,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
+ "Removing watched videos, partially watched=" + removePartiallyWatched))));
}
@Override
@@ -511,17 +512,6 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
{ /*Do nothing on success*/ }, this::onError);
+ .subscribe(longs -> { /*Do nothing on success*/ }, throwable ->
+ showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
+ "Renaming playlist")));
disposables.add(disposable);
}
@@ -583,7 +575,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
successToast.show(), this::onError);
+ .subscribe(ignore -> successToast.show(), throwable ->
+ showError(new ErrorInfo(throwable, UserAction.REQUESTED_BOOKMARK,
+ "Changing playlist thumbnail")));
disposables.add(disposable);
}
@@ -632,7 +626,9 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
saveImmediate(), this::onError);
+ .subscribe(ignored -> saveImmediate(), throwable ->
+ showError(new ErrorInfo(throwable, UserAction.SOMETHING_ELSE,
+ "Debounced saver")));
}
private void saveImmediate() {
@@ -669,7 +665,8 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
showError(new ErrorInfo(throwable,
+ UserAction.REQUESTED_BOOKMARK, "Saving playlist"))
);
disposables.add(disposable);
}
@@ -683,7 +680,7 @@ public class LocalPlaylistFragment extends BaseLocalListFragment
() {
binding.itemsList.adapter = groupAdapter
viewModel = ViewModelProvider(this).get(SubscriptionViewModel::class.java)
- viewModel.stateLiveData.observe(viewLifecycleOwner, { it?.let(this::handleResult) })
- viewModel.feedGroupsLiveData.observe(viewLifecycleOwner, { it?.let(this::handleFeedGroups) })
+ viewModel.stateLiveData.observe(viewLifecycleOwner) { it?.let(this::handleResult) }
+ viewModel.feedGroupsLiveData.observe(viewLifecycleOwner) { it?.let(this::handleFeedGroups) }
}
private fun showLongTapDialog(selectedItem: ChannelInfoItem) {
@@ -381,7 +382,9 @@ class SubscriptionFragment : BaseStateFragment
+ *
+ *
+ * @return the maximum height for the end screen thumbnail
+ */
+ private float calculateMaxEndScreenThumbnailHeight() {
+ // ensure that screenHeight is initialized and thus not 0
+ updateScreenSize();
+
+ if (DeviceUtils.isTv(context) && !isFullscreen) {
+ final int videoInfoHeight =
+ DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(16, context);
+ return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight);
+ } else if (DeviceUtils.isTablet(context) && service.isLandscape() && !isFullscreen) {
+ final int videoInfoHeight =
+ DeviceUtils.dpToPx(85, context) + DeviceUtils.spToPx(15, context);
+ return Math.min(currentThumbnail.getHeight(), screenHeight - videoInfoHeight);
+ } else { // fullscreen player: max height is the device height
+ return Math.min(currentThumbnail.getHeight(), screenHeight);
+ }
+ }
+
@Override
public void onLoadingStarted(final String imageUri, final View view) {
if (DEBUG) {
@@ -1207,23 +1286,29 @@ public final class Player implements
@Override
public void onLoadingComplete(final String imageUri, final View view,
final Bitmap loadedImage) {
- final float width = Math.min(
+ // scale down the notification thumbnail for performance
+ final float notificationThumbnailWidth = Math.min(
context.getResources().getDimension(R.dimen.player_notification_thumbnail_width),
loadedImage.getWidth());
+ currentThumbnail = Bitmap.createScaledBitmap(
+ loadedImage,
+ (int) notificationThumbnailWidth,
+ (int) (loadedImage.getHeight()
+ / (loadedImage.getWidth() / notificationThumbnailWidth)),
+ true);
if (DEBUG) {
Log.d(TAG, "Thumbnail - onLoadingComplete() called with: "
+ "imageUri = [" + imageUri + "], view = [" + view + "], "
+ "loadedImage = [" + loadedImage + "], "
+ loadedImage.getWidth() + "x" + loadedImage.getHeight()
- + ", scaled width = " + width);
+ + ", scaled notification width = " + notificationThumbnailWidth);
}
- currentThumbnail = Bitmap.createScaledBitmap(loadedImage,
- (int) width,
- (int) (loadedImage.getHeight() / (loadedImage.getWidth() / width)), true);
- binding.endScreen.setImageBitmap(loadedImage);
NotificationUtil.getInstance().createNotificationIfNeededAndUpdate(this, false);
+
+ // there is a new thumbnail, thus the end screen thumbnail needs to be changed, too.
+ updateEndScreenThumbnail();
}
@Override
@@ -1432,7 +1517,8 @@ public final class Player implements
}
public boolean getPlaybackSkipSilence() {
- return getPlaybackParameters().skipSilence;
+ return !exoPlayerIsNull() && simpleExoPlayer.getAudioComponent() != null
+ && simpleExoPlayer.getAudioComponent().getSkipSilenceEnabled();
}
public PlaybackParameters getPlaybackParameters() {
@@ -1457,7 +1543,10 @@ public final class Player implements
savePlaybackParametersToPrefs(this, roundedSpeed, roundedPitch, skipSilence);
simpleExoPlayer.setPlaybackParameters(
- new PlaybackParameters(roundedSpeed, roundedPitch, skipSilence));
+ new PlaybackParameters(roundedSpeed, roundedPitch));
+ if (simpleExoPlayer.getAudioComponent() != null) {
+ simpleExoPlayer.getAudioComponent().setSkipSilenceEnabled(skipSilence);
+ }
}
//endregion
@@ -2333,6 +2422,7 @@ public final class Player implements
case ExoPlaybackException.TYPE_OUT_OF_MEMORY:
case ExoPlaybackException.TYPE_REMOTE:
case ExoPlaybackException.TYPE_RENDERER:
+ case ExoPlaybackException.TYPE_TIMEOUT:
default:
showUnrecoverableError(error);
onPlaybackShutdown();
@@ -3355,7 +3445,7 @@ public final class Player implements
final List85dp free space for {@link R.id.detail_root}
+ * and additional space for the stream title text size
+ * ({@link R.id.detail_title_root_layout}).
+ * The text size is 15sp on tablets and 16sp on TVs,
+ * see {@link R.id.titleTextView}.
+ * > getSubscriptionObserver() {
return new Observer
>() {
@Override
- public void onSubscribe(final Disposable d) { }
+ public void onSubscribe(@NonNull final Disposable disposable) { }
@Override
- public void onNext(final List