diff --git a/.github/workflows/build-release-apk.yml b/.github/workflows/build-release-apk.yml
index 0fad8e1695e..73a2dd2dcb7 100644
--- a/.github/workflows/build-release-apk.yml
+++ b/.github/workflows/build-release-apk.yml
@@ -1,15 +1,22 @@
-name: "Build unsigned release APK on master"
+name: "Build and Release APK"
on:
workflow_dispatch:
+ push:
+ branches:
+ - master
+ - main
+ - dev
+ tags:
+ - 'v*'
jobs:
release:
runs-on: ubuntu-latest
+ permissions:
+ contents: write
steps:
- uses: actions/checkout@v4
- with:
- ref: 'master'
- uses: actions/setup-java@v4
with:
@@ -17,22 +24,55 @@ jobs:
java-version: '21'
cache: 'gradle'
+ - name: "Create debug keystore for signing"
+ run: |
+ mkdir -p ~/.android
+ keytool -genkey -v -keystore ~/.android/debug.keystore -storepass android -alias androiddebugkey -keypass android -keyalg RSA -keysize 2048 -validity 10000 -dname "CN=Android Debug,O=Android,C=US"
+
- name: "Build release APK"
run: ./gradlew assembleRelease --stacktrace
- - name: "Rename APK"
+ - name: "Get version name"
+ id: version
run: |
VERSION_NAME="$(jq -r ".elements[0].versionName" "app/build/outputs/apk/release/output-metadata.json")"
+ echo "version=$VERSION_NAME" >> "$GITHUB_OUTPUT"
echo "Version name: $VERSION_NAME" >> "$GITHUB_STEP_SUMMARY"
echo '```json' >> "$GITHUB_STEP_SUMMARY"
cat "app/build/outputs/apk/release/output-metadata.json" >> "$GITHUB_STEP_SUMMARY"
echo >> "$GITHUB_STEP_SUMMARY"
echo '```' >> "$GITHUB_STEP_SUMMARY"
- # assume there is only one APK in that folder
- mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipe_v$VERSION_NAME.apk"
- - name: "Upload APK"
+ - name: "Rename APK"
+ run: |
+ mv app/build/outputs/apk/release/*.apk "app/build/outputs/apk/release/NewPipeScrolling_v${{ steps.version.outputs.version }}.apk"
+
+ - name: "Upload APK as artifact"
uses: actions/upload-artifact@v4
with:
name: app
path: app/build/outputs/apk/release/*.apk
+
+ - name: "Create Release"
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: v${{ steps.version.outputs.version }}-${{ github.run_number }}
+ name: NewPipe Scrolling v${{ steps.version.outputs.version }}
+ body: |
+ ## NewPipe Scrolling v${{ steps.version.outputs.version }}
+
+ This is a modified version of NewPipe that can be installed alongside the original app.
+
+ ### New Features
+ - **Hold to Fast-Forward**: Hold anywhere on the player to increase playback speed to 2x. Release to return to normal speed.
+ - New setting in Video & Audio settings to enable/disable this feature.
+
+ ### Installation
+ 1. Download the APK below
+ 2. Install on your Android device (enable "Install from unknown sources" if needed)
+ 3. This app will appear as "NewPipe Scrolling" and can coexist with the original NewPipe
+ files: app/build/outputs/apk/release/*.apk
+ draft: false
+ prerelease: false
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 5cf357c7458..4904d91529b 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -36,9 +36,19 @@ android {
compileSdk = 36
namespace = "org.schabi.newpipe"
+ signingConfigs {
+ create("release") {
+ // Use debug keystore for unsigned releases that can be installed
+ storeFile = file("${System.getProperty("user.home")}/.android/debug.keystore")
+ storePassword = "android"
+ keyAlias = "androiddebugkey"
+ keyPassword = "android"
+ }
+ }
+
defaultConfig {
- applicationId = "org.schabi.newpipe"
- resValue("string", "app_name", "NewPipe")
+ applicationId = "org.schabi.newpipe.scrolling"
+ resValue("string", "app_name", "NewPipe Scrolling")
minSdk = 21
targetSdk = 35
@@ -72,9 +82,10 @@ android {
}
release {
+ signingConfig = signingConfigs.getByName("release")
System.getProperty("packageSuffix")?.let { suffix ->
applicationIdSuffix = suffix
- resValue("string", "app_name", "NewPipe $suffix")
+ resValue("string", "app_name", "NewPipe Scrolling $suffix")
}
isMinifyEnabled = true
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
diff --git a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
index 0453f297a4f..023c6cdfa71 100644
--- a/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
+++ b/app/src/main/java/org/schabi/newpipe/player/gesture/BasePlayerGestureListener.kt
@@ -9,6 +9,7 @@ import android.view.View
import androidx.core.os.postDelayed
import org.schabi.newpipe.databinding.PlayerBinding
import org.schabi.newpipe.player.Player
+import org.schabi.newpipe.player.helper.PlayerHelper
import org.schabi.newpipe.player.ui.VideoPlayerUi
/**
@@ -24,8 +25,59 @@ abstract class BasePlayerGestureListener(
protected val player: Player = playerUi.player
protected val binding: PlayerBinding = playerUi.binding
+ // ///////////////////////////////////////////////////////////////////
+ // Long press (hold) to fast-forward
+ // ///////////////////////////////////////////////////////////////////
+
+ private var isHoldingForFastForward = false
+ private var originalPlaybackSpeed = 1.0f
+ private val longPressHandler: Handler = Handler(Looper.getMainLooper())
+ private val longPressRunnable = Runnable {
+ if (PlayerHelper.isHoldToFastForwardEnabled(player.context) &&
+ player.currentState == Player.STATE_PLAYING &&
+ !playerUi.isSomePopupMenuVisible
+ ) {
+ startFastForward()
+ }
+ }
+
+ private fun startFastForward() {
+ if (!isHoldingForFastForward) {
+ if (DEBUG) {
+ Log.d(TAG, "startFastForward called")
+ }
+ isHoldingForFastForward = true
+ originalPlaybackSpeed = player.playbackSpeed
+ player.setPlaybackSpeed(2.0f)
+ }
+ }
+
+ private fun stopFastForward() {
+ if (isHoldingForFastForward) {
+ if (DEBUG) {
+ Log.d(TAG, "stopFastForward called, restoring speed to $originalPlaybackSpeed")
+ }
+ isHoldingForFastForward = false
+ player.setPlaybackSpeed(originalPlaybackSpeed)
+ }
+ longPressHandler.removeCallbacks(longPressRunnable)
+ }
+
override fun onTouch(v: View, event: MotionEvent): Boolean {
playerUi.gestureDetector.onTouchEvent(event)
+
+ // Handle long press for fast-forward
+ when (event.action) {
+ MotionEvent.ACTION_DOWN -> {
+ // Start timer for long press detection
+ longPressHandler.postDelayed(longPressRunnable, LONG_PRESS_DELAY)
+ }
+ MotionEvent.ACTION_UP, MotionEvent.ACTION_CANCEL -> {
+ // Stop fast-forward when finger is lifted
+ stopFastForward()
+ }
+ }
+
return false
}
@@ -184,5 +236,6 @@ abstract class BasePlayerGestureListener(
private const val DOUBLE_TAP = "doubleTap"
private const val DOUBLE_TAP_DELAY = 550L
+ private const val LONG_PRESS_DELAY = 500L
}
}
diff --git a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
index c335e9b7c60..6fe7e5a5a86 100644
--- a/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
+++ b/app/src/main/java/org/schabi/newpipe/player/helper/PlayerHelper.java
@@ -226,6 +226,11 @@ public static boolean isStartMainPlayerFullscreenEnabled(@NonNull final Context
.getBoolean(context.getString(R.string.start_main_player_fullscreen_key), false);
}
+ public static boolean isHoldToFastForwardEnabled(@NonNull final Context context) {
+ return getPreferences(context)
+ .getBoolean(context.getString(R.string.hold_to_fast_forward_key), true);
+ }
+
public static boolean isAutoQueueEnabled(@NonNull final Context context) {
return getPreferences(context)
.getBoolean(context.getString(R.string.auto_queue_key), false);
diff --git a/app/src/main/res/values/settings_keys.xml b/app/src/main/res/values/settings_keys.xml
index 352e4cec120..ad7c0342f68 100644
--- a/app/src/main/res/values/settings_keys.xml
+++ b/app/src/main/res/values/settings_keys.xml
@@ -24,6 +24,7 @@
screen_brightness_timestamp_key
clear_queue_confirmation_key
ignore_hardware_media_buttons_key
+ hold_to_fast_forward_key
popup_saved_width
popup_saved_x
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 147c88938a9..9b31f43c0c7 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -91,6 +91,8 @@
The active player queue will be replaced
Ignore hardware media button events
Useful, for instance, if you are using a headset with broken physical buttons
+ Hold to fast-forward
+ Hold anywhere on the player to increase playback speed to 2x. Release to return to normal speed
Show comments
Turn off to hide comments
Show \'Next\' and \'Similar\' videos
diff --git a/app/src/main/res/xml/video_audio_settings.xml b/app/src/main/res/xml/video_audio_settings.xml
index 727ce4df40a..b24b034beac 100644
--- a/app/src/main/res/xml/video_audio_settings.xml
+++ b/app/src/main/res/xml/video_audio_settings.xml
@@ -249,5 +249,13 @@
android:title="@string/ignore_hardware_media_buttons_title"
app:singleLineTitle="false"
app:iconSpaceReserved="false" />
+
+