SponsorBlock: started work on a local player implementation
This commit is contained in:
parent
991c12c539
commit
420f99f317
16 changed files with 460 additions and 26 deletions
|
@ -22,8 +22,8 @@
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
android:logo="@mipmap/ic_launcher"
|
android:logo="@mipmap/ic_launcher"
|
||||||
android:requestLegacyExternalStorage="true"
|
android:requestLegacyExternalStorage="true"
|
||||||
android:theme="@style/OpeningTheme"
|
|
||||||
android:resizeableActivity="true"
|
android:resizeableActivity="true"
|
||||||
|
android:theme="@style/OpeningTheme"
|
||||||
tools:ignore="AllowBackup">
|
tools:ignore="AllowBackup">
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
|
@ -56,11 +56,9 @@
|
||||||
android:name=".player.BackgroundPlayerActivity"
|
android:name=".player.BackgroundPlayerActivity"
|
||||||
android:label="@string/title_activity_play_queue"
|
android:label="@string/title_activity_play_queue"
|
||||||
android:launchMode="singleTask" />
|
android:launchMode="singleTask" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".settings.SettingsActivity"
|
android:name=".settings.SettingsActivity"
|
||||||
android:label="@string/settings" />
|
android:label="@string/settings" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".about.AboutActivity"
|
android:name=".about.AboutActivity"
|
||||||
android:label="@string/title_activity_about" />
|
android:label="@string/title_activity_about" />
|
||||||
|
@ -80,14 +78,11 @@
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ExitActivity"
|
android:name=".ExitActivity"
|
||||||
android:label="@string/general_error"
|
android:label="@string/general_error"
|
||||||
android:theme="@android:style/Theme.NoDisplay" />
|
android:theme="@android:style/Theme.NoDisplay" />
|
||||||
<activity android:name=".report.ErrorActivity" />
|
<activity android:name=".report.ErrorActivity" /> <!-- giga get related -->
|
||||||
|
|
||||||
<!-- giga get related -->
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".download.DownloadActivity"
|
android:name=".download.DownloadActivity"
|
||||||
android:label="@string/app_name"
|
android:label="@string/app_name"
|
||||||
|
@ -101,10 +96,10 @@
|
||||||
android:theme="@style/FilePickerThemeDark">
|
android:theme="@style/FilePickerThemeDark">
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.GET_CONTENT" />
|
<action android:name="android.intent.action.GET_CONTENT" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ReCaptchaActivity"
|
android:name=".ReCaptchaActivity"
|
||||||
android:label="@string/recaptcha" />
|
android:label="@string/recaptcha" />
|
||||||
|
@ -262,7 +257,9 @@
|
||||||
<!-- Share filter -->
|
<!-- Share filter -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.SEND" />
|
<action android:name="android.intent.action.SEND" />
|
||||||
|
|
||||||
<category android:name="android.intent.category.DEFAULT" />
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
|
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
|
||||||
|
@ -280,7 +277,7 @@
|
||||||
<data android:host="media.ccc.de" />
|
<data android:host="media.ccc.de" />
|
||||||
<!-- video prefix -->
|
<!-- video prefix -->
|
||||||
<data android:pathPrefix="/v/" />
|
<data android:pathPrefix="/v/" />
|
||||||
<!-- channel prefix-->
|
<!-- channel prefix -->
|
||||||
<data android:pathPrefix="/c/" />
|
<data android:pathPrefix="/c/" />
|
||||||
<data android:pathPrefix="/b/" />
|
<data android:pathPrefix="/b/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
|
@ -296,7 +293,6 @@
|
||||||
|
|
||||||
<data android:scheme="http" />
|
<data android:scheme="http" />
|
||||||
<data android:scheme="https" />
|
<data android:scheme="https" />
|
||||||
|
|
||||||
<data android:host="framatube.org" />
|
<data android:host="framatube.org" />
|
||||||
<data android:host="media.assassinate-you.net" />
|
<data android:host="media.assassinate-you.net" />
|
||||||
<data android:host="peertube.co.uk" />
|
<data android:host="peertube.co.uk" />
|
||||||
|
@ -308,20 +304,28 @@
|
||||||
<data android:host="video.ploud.fr" />
|
<data android:host="video.ploud.fr" />
|
||||||
<data android:host="video.lqdn.fr" />
|
<data android:host="video.lqdn.fr" />
|
||||||
<data android:host="skeptikon.fr" />
|
<data android:host="skeptikon.fr" />
|
||||||
|
|
||||||
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
<data android:pathPrefix="/videos/" /> <!-- it contains playlists -->
|
||||||
<data android:pathPrefix="/accounts/" />
|
<data android:pathPrefix="/accounts/" />
|
||||||
<data android:pathPrefix="/video-channels/" />
|
<data android:pathPrefix="/video-channels/" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<activity
|
||||||
|
android:name=".LocalPlayerActivity"
|
||||||
|
android:label="@string/app_name"
|
||||||
|
android:launchMode="singleTask"
|
||||||
|
android:foregroundServiceType="mediaPlayback" />
|
||||||
|
|
||||||
<service
|
<service
|
||||||
android:name=".RouterActivity$FetcherService"
|
android:name=".RouterActivity$FetcherService"
|
||||||
android:exported="false" />
|
android:exported="false" /> <!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
||||||
|
|
||||||
<!-- see https://github.com/TeamNewPipe/NewPipe/issues/3947 -->
|
|
||||||
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
<!-- Version < 3.0. DeX Mode and Screen Mirroring support -->
|
||||||
<meta-data android:name="com.samsung.android.keepalive.density" android:value="true"/>
|
<meta-data
|
||||||
<!-- Version >= 3.0. DeX Dual Mode support -->
|
android:name="com.samsung.android.keepalive.density"
|
||||||
<meta-data android:name="com.samsung.android.multidisplay.keep_process_alive" android:value="true"/>
|
android:value="true" /> <!-- Version >= 3.0. DeX Dual Mode support -->
|
||||||
|
<meta-data
|
||||||
|
android:name="com.samsung.android.multidisplay.keep_process_alive"
|
||||||
|
android:value="true" />
|
||||||
</application>
|
</application>
|
||||||
|
|
||||||
</manifest>
|
</manifest>
|
311
app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java
Normal file
311
app/src/main/java/org/schabi/newpipe/LocalPlayerActivity.java
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
import android.net.Uri;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.util.Log;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.google.android.exoplayer2.Player;
|
||||||
|
import com.google.android.exoplayer2.SeekParameters;
|
||||||
|
import com.google.android.exoplayer2.SimpleExoPlayer;
|
||||||
|
import com.google.android.exoplayer2.source.MediaSource;
|
||||||
|
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
|
||||||
|
import com.google.android.exoplayer2.ui.PlayerView;
|
||||||
|
import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.player.helper.PlayerHelper;
|
||||||
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
|
||||||
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.rxjava3.core.Observable;
|
||||||
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
|
import io.reactivex.rxjava3.disposables.SerialDisposable;
|
||||||
|
|
||||||
|
import static java.util.concurrent.TimeUnit.MILLISECONDS;
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_BLOCKED;
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_BUFFERING;
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_COMPLETED;
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_PAUSED;
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_PAUSED_SEEK;
|
||||||
|
import static org.schabi.newpipe.player.BasePlayer.STATE_PLAYING;
|
||||||
|
|
||||||
|
public class LocalPlayerActivity extends AppCompatActivity implements Player.EventListener {
|
||||||
|
private SimpleExoPlayer simpleExoPlayer;
|
||||||
|
private SerialDisposable progressUpdateReactor;
|
||||||
|
private int lastCurrentProgress = -1;
|
||||||
|
protected static final int PROGRESS_LOOP_INTERVAL_MILLIS = 500;
|
||||||
|
public static final String TAG = "LocalPlayer";
|
||||||
|
private VideoSegment[] segments;
|
||||||
|
private SharedPreferences mPrefs;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
setContentView(R.layout.activity_local_player);
|
||||||
|
|
||||||
|
this.mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
|
||||||
|
|
||||||
|
initPlayer();
|
||||||
|
initSegments();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initPlayer() {
|
||||||
|
final String uri = getIntent().getDataString();
|
||||||
|
|
||||||
|
if (uri == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleExoPlayer = new SimpleExoPlayer
|
||||||
|
.Builder(this)
|
||||||
|
.build();
|
||||||
|
simpleExoPlayer.addListener(this);
|
||||||
|
simpleExoPlayer.setSeekParameters(PlayerHelper.getSeekParameters(this));
|
||||||
|
simpleExoPlayer.setHandleAudioBecomingNoisy(true);
|
||||||
|
final MediaSource videoSource = new ProgressiveMediaSource
|
||||||
|
.Factory(new DefaultDataSourceFactory(this, DownloaderImpl.USER_AGENT))
|
||||||
|
.createMediaSource(Uri.parse(uri));
|
||||||
|
simpleExoPlayer.prepare(videoSource);
|
||||||
|
final PlayerView playerView = findViewById(R.id.player_view);
|
||||||
|
playerView.setPlayer(simpleExoPlayer);
|
||||||
|
|
||||||
|
this.progressUpdateReactor = new SerialDisposable();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void initSegments() {
|
||||||
|
final String segmentsJson = getIntent().getStringExtra("segments");
|
||||||
|
|
||||||
|
if (segmentsJson != null && segmentsJson.length() > 0) {
|
||||||
|
try {
|
||||||
|
final ArrayList<VideoSegment> segmentsArrayList = new ArrayList<>();
|
||||||
|
final JsonObject obj = JsonParser.object().from(segmentsJson);
|
||||||
|
|
||||||
|
for (final Object item : obj.getArray("segments")) {
|
||||||
|
final JsonObject itemObject = (JsonObject) item;
|
||||||
|
|
||||||
|
final double startTime = itemObject.getDouble("start");
|
||||||
|
final double endTime = itemObject.getDouble("end");
|
||||||
|
final String category = itemObject.getString("category");
|
||||||
|
|
||||||
|
final VideoSegment segment = new VideoSegment(startTime, endTime, category);
|
||||||
|
segmentsArrayList.add(segment);
|
||||||
|
}
|
||||||
|
|
||||||
|
segments = segmentsArrayList.toArray(new VideoSegment[0]);
|
||||||
|
} catch (final Exception e) {
|
||||||
|
Log.e(TAG, "Error initializing segments", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
simpleExoPlayer.removeListener(this);
|
||||||
|
simpleExoPlayer.stop();
|
||||||
|
simpleExoPlayer.release();
|
||||||
|
progressUpdateReactor.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPlayerStateChanged(final boolean playWhenReady, final int playbackState) {
|
||||||
|
switch (playbackState) {
|
||||||
|
case Player.STATE_IDLE:
|
||||||
|
break;
|
||||||
|
case Player.STATE_BUFFERING:
|
||||||
|
break;
|
||||||
|
case Player.STATE_READY:
|
||||||
|
changeState(playWhenReady ? STATE_PLAYING : STATE_PAUSED);
|
||||||
|
break;
|
||||||
|
case Player.STATE_ENDED:
|
||||||
|
changeState(STATE_COMPLETED);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void changeState(final int state) {
|
||||||
|
switch (state) {
|
||||||
|
case STATE_BLOCKED:
|
||||||
|
onBlocked();
|
||||||
|
break;
|
||||||
|
case STATE_PLAYING:
|
||||||
|
onPlaying();
|
||||||
|
break;
|
||||||
|
case STATE_BUFFERING:
|
||||||
|
onBuffering();
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED:
|
||||||
|
onPaused();
|
||||||
|
break;
|
||||||
|
case STATE_PAUSED_SEEK:
|
||||||
|
onPausedSeek();
|
||||||
|
break;
|
||||||
|
case STATE_COMPLETED:
|
||||||
|
onCompleted();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBlocked() {
|
||||||
|
if (!isProgressLoopRunning()) {
|
||||||
|
startProgressLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPlaying() {
|
||||||
|
if (!isProgressLoopRunning()) {
|
||||||
|
startProgressLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onBuffering() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPaused() {
|
||||||
|
if (isProgressLoopRunning()) {
|
||||||
|
stopProgressLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onPausedSeek() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private void onCompleted() {
|
||||||
|
if (isProgressLoopRunning()) {
|
||||||
|
stopProgressLoop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isProgressLoopRunning() {
|
||||||
|
return progressUpdateReactor.get() != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void startProgressLoop() {
|
||||||
|
progressUpdateReactor.set(getProgressReactor());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void stopProgressLoop() {
|
||||||
|
progressUpdateReactor.set(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Disposable getProgressReactor() {
|
||||||
|
return Observable.interval(PROGRESS_LOOP_INTERVAL_MILLIS, MILLISECONDS,
|
||||||
|
AndroidSchedulers.mainThread())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe(ignored -> triggerProgressUpdate(),
|
||||||
|
error -> Log.e(TAG, "Progress update failure: ", error));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void triggerProgressUpdate() {
|
||||||
|
if (simpleExoPlayer == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final int currentProgress = Math.max((int) simpleExoPlayer.getCurrentPosition(), 0);
|
||||||
|
|
||||||
|
final boolean isRewind = currentProgress < lastCurrentProgress;
|
||||||
|
|
||||||
|
lastCurrentProgress = currentProgress;
|
||||||
|
|
||||||
|
if (!mPrefs.getBoolean(
|
||||||
|
this.getString(R.string.sponsor_block_enable_key), false)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final VideoSegment segment = getSkippableSegment(currentProgress);
|
||||||
|
if (segment == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
int skipTarget = isRewind
|
||||||
|
? (int) Math.ceil((segment.startTime)) - 1
|
||||||
|
: (int) Math.ceil((segment.endTime));
|
||||||
|
|
||||||
|
if (skipTarget < 0) {
|
||||||
|
skipTarget = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// temporarily force EXACT seek parameters to prevent infinite skip looping
|
||||||
|
final SeekParameters seekParams = simpleExoPlayer.getSeekParameters();
|
||||||
|
simpleExoPlayer.setSeekParameters(SeekParameters.EXACT);
|
||||||
|
|
||||||
|
seekTo(skipTarget);
|
||||||
|
|
||||||
|
simpleExoPlayer.setSeekParameters(seekParams);
|
||||||
|
|
||||||
|
if (mPrefs.getBoolean(
|
||||||
|
this.getString(R.string.sponsor_block_notifications_key), false)) {
|
||||||
|
String toastText = "";
|
||||||
|
|
||||||
|
switch (segment.category) {
|
||||||
|
case "sponsor":
|
||||||
|
toastText = this
|
||||||
|
.getString(R.string.sponsor_block_skip_sponsor_toast);
|
||||||
|
break;
|
||||||
|
case "intro":
|
||||||
|
toastText = this
|
||||||
|
.getString(R.string.sponsor_block_skip_intro_toast);
|
||||||
|
break;
|
||||||
|
case "outro":
|
||||||
|
toastText = this
|
||||||
|
.getString(R.string.sponsor_block_skip_outro_toast);
|
||||||
|
break;
|
||||||
|
case "interaction":
|
||||||
|
toastText = this
|
||||||
|
.getString(R.string.sponsor_block_skip_interaction_toast);
|
||||||
|
break;
|
||||||
|
case "selfpromo":
|
||||||
|
toastText = this
|
||||||
|
.getString(R.string.sponsor_block_skip_self_promo_toast);
|
||||||
|
break;
|
||||||
|
case "music_offtopic":
|
||||||
|
toastText = this
|
||||||
|
.getString(R.string.sponsor_block_skip_non_music_toast);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
Toast.makeText(this, toastText, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void seekTo(final long positionMillis) {
|
||||||
|
if (simpleExoPlayer != null) {
|
||||||
|
// prevent invalid positions when fast-forwarding/-rewinding
|
||||||
|
long normalizedPositionMillis = positionMillis;
|
||||||
|
if (normalizedPositionMillis < 0) {
|
||||||
|
normalizedPositionMillis = 0;
|
||||||
|
} else if (normalizedPositionMillis > simpleExoPlayer.getDuration()) {
|
||||||
|
normalizedPositionMillis = simpleExoPlayer.getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
simpleExoPlayer.seekTo(normalizedPositionMillis);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public VideoSegment getSkippableSegment(final int progress) {
|
||||||
|
if (segments == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (final VideoSegment segment : segments) {
|
||||||
|
if (progress < segment.startTime) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (progress > segment.endTime) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return segment;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
|
@ -60,6 +60,7 @@ import org.schabi.newpipe.util.SecondaryStreamHelper;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter;
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -124,6 +125,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
|
|
||||||
private SharedPreferences prefs;
|
private SharedPreferences prefs;
|
||||||
|
|
||||||
|
private VideoSegment[] segments;
|
||||||
|
|
||||||
public static DownloadDialog newInstance(final StreamInfo info) {
|
public static DownloadDialog newInstance(final StreamInfo info) {
|
||||||
final DownloadDialog dialog = new DownloadDialog();
|
final DownloadDialog dialog = new DownloadDialog();
|
||||||
dialog.setInfo(info);
|
dialog.setInfo(info);
|
||||||
|
@ -190,6 +193,10 @@ public class DownloadDialog extends DialogFragment
|
||||||
this.selectedSubtitleIndex = ssi;
|
this.selectedSubtitleIndex = ssi;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setVideoSegments(final VideoSegment[] seg) {
|
||||||
|
this.segments = seg;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
public void onCreate(@Nullable final Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
@ -942,7 +949,8 @@ public class DownloadDialog extends DialogFragment
|
||||||
}
|
}
|
||||||
|
|
||||||
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
DownloadManagerService.startMission(context, urls, storage, kind, threads,
|
||||||
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo);
|
currentInfo.getUrl(), psName, psArgs, nearLength, recoveryInfo,
|
||||||
|
segments);
|
||||||
|
|
||||||
dismiss();
|
dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1678,6 +1678,7 @@ public final class VideoDetailFragment
|
||||||
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||||
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||||
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
downloadDialog.setSubtitleStreams(currentInfo.getSubtitles());
|
||||||
|
downloadDialog.setVideoSegments(player.getVideoSegments());
|
||||||
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package org.schabi.newpipe.util;
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
public class VideoSegment {
|
import java.io.Serializable;
|
||||||
|
|
||||||
|
public class VideoSegment implements Serializable {
|
||||||
public double startTime;
|
public double startTime;
|
||||||
public double endTime;
|
public double endTime;
|
||||||
public String category;
|
public String category;
|
||||||
|
|
|
@ -13,6 +13,7 @@ public class FinishedMission extends Mission {
|
||||||
timestamp = mission.timestamp;
|
timestamp = mission.timestamp;
|
||||||
kind = mission.kind;
|
kind = mission.kind;
|
||||||
storage = mission.storage;
|
storage = mission.storage;
|
||||||
|
segmentsJson = mission.segmentsJson;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,8 @@ package us.shandian.giga.get;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.Calendar;
|
import java.util.Calendar;
|
||||||
|
|
||||||
|
@ -35,6 +37,8 @@ public abstract class Mission implements Serializable {
|
||||||
*/
|
*/
|
||||||
public StoredFileHelper storage;
|
public StoredFileHelper storage;
|
||||||
|
|
||||||
|
public String segmentsJson;
|
||||||
|
|
||||||
public long getTimestamp() {
|
public long getTimestamp() {
|
||||||
return timestamp;
|
return timestamp;
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,7 +26,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
// TODO: use NewPipeSQLiteHelper ('s constants) when playlist branch is merged (?)
|
||||||
private static final String DATABASE_NAME = "downloads.db";
|
private static final String DATABASE_NAME = "downloads.db";
|
||||||
|
|
||||||
private static final int DATABASE_VERSION = 4;
|
private static final int DATABASE_VERSION = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The table name of download missions (old)
|
* The table name of download missions (old)
|
||||||
|
@ -55,6 +55,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
|
|
||||||
private static final String KEY_PATH = "path";
|
private static final String KEY_PATH = "path";
|
||||||
|
|
||||||
|
private static final String KEY_SEGMENTS = "segments";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The statement to create the table
|
* The statement to create the table
|
||||||
*/
|
*/
|
||||||
|
@ -65,6 +67,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
KEY_DONE + " INTEGER NOT NULL, " +
|
KEY_DONE + " INTEGER NOT NULL, " +
|
||||||
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
KEY_TIMESTAMP + " INTEGER NOT NULL, " +
|
||||||
KEY_KIND + " TEXT NOT NULL, " +
|
KEY_KIND + " TEXT NOT NULL, " +
|
||||||
|
KEY_SEGMENTS + " TEXT, " +
|
||||||
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
|
" UNIQUE(" + KEY_TIMESTAMP + ", " + KEY_PATH + "));";
|
||||||
|
|
||||||
|
|
||||||
|
@ -120,6 +123,11 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
|
|
||||||
cursor.close();
|
cursor.close();
|
||||||
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
|
db.execSQL("DROP TABLE " + MISSIONS_TABLE_NAME_v2);
|
||||||
|
oldVersion++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (oldVersion == 4) {
|
||||||
|
db.execSQL("ALTER TABLE " + FINISHED_TABLE_NAME + " ADD COLUMN " + KEY_SEGMENTS + " TEXT;");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,6 +144,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
values.put(KEY_DONE, downloadMission.length);
|
values.put(KEY_DONE, downloadMission.length);
|
||||||
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
values.put(KEY_TIMESTAMP, downloadMission.timestamp);
|
||||||
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
values.put(KEY_KIND, String.valueOf(downloadMission.kind));
|
||||||
|
values.put(KEY_SEGMENTS, downloadMission.segmentsJson);
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -153,6 +162,7 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
||||||
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
mission.length = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_DONE));
|
||||||
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
mission.timestamp = cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP));
|
||||||
mission.kind = kind.charAt(0);
|
mission.kind = kind.charAt(0);
|
||||||
|
mission.segmentsJson = cursor.getString(cursor.getColumnIndexOrThrow(KEY_SEGMENTS));
|
||||||
|
|
||||||
try {
|
try {
|
||||||
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
|
mission.storage = new StoredFileHelper(context,null, Uri.parse(path), "");
|
||||||
|
|
|
@ -38,9 +38,13 @@ import androidx.annotation.StringRes;
|
||||||
import androidx.core.app.NotificationCompat;
|
import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.app.NotificationCompat.Builder;
|
import androidx.core.app.NotificationCompat.Builder;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonStringWriter;
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.download.DownloadActivity;
|
import org.schabi.newpipe.download.DownloadActivity;
|
||||||
import org.schabi.newpipe.player.helper.LockManager;
|
import org.schabi.newpipe.player.helper.LockManager;
|
||||||
|
import org.schabi.newpipe.util.VideoSegment;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
@ -80,6 +84,7 @@ public class DownloadManagerService extends Service {
|
||||||
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
private static final String EXTRA_PARENT_PATH = "DownloadManagerService.extra.storageParentPath";
|
||||||
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
private static final String EXTRA_STORAGE_TAG = "DownloadManagerService.extra.storageTag";
|
||||||
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
private static final String EXTRA_RECOVERY_INFO = "DownloadManagerService.extra.recoveryInfo";
|
||||||
|
private static final String EXTRA_SEGMENTS = "DownloadManagerService.extra.segments";
|
||||||
|
|
||||||
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
private static final String ACTION_RESET_DOWNLOAD_FINISHED = APPLICATION_ID + ".reset_download_finished";
|
||||||
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
private static final String ACTION_OPEN_DOWNLOADS_FINISHED = APPLICATION_ID + ".open_downloads_finished";
|
||||||
|
@ -388,7 +393,8 @@ public class DownloadManagerService extends Service {
|
||||||
*/
|
*/
|
||||||
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
public static void startMission(Context context, String[] urls, StoredFileHelper storage,
|
||||||
char kind, int threads, String source, String psName,
|
char kind, int threads, String source, String psName,
|
||||||
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo) {
|
String[] psArgs, long nearLength, MissionRecoveryInfo[] recoveryInfo,
|
||||||
|
VideoSegment[] segments) {
|
||||||
Intent intent = new Intent(context, DownloadManagerService.class);
|
Intent intent = new Intent(context, DownloadManagerService.class);
|
||||||
intent.setAction(Intent.ACTION_RUN);
|
intent.setAction(Intent.ACTION_RUN);
|
||||||
intent.putExtra(EXTRA_URLS, urls);
|
intent.putExtra(EXTRA_URLS, urls);
|
||||||
|
@ -403,6 +409,7 @@ public class DownloadManagerService extends Service {
|
||||||
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
|
intent.putExtra(EXTRA_PARENT_PATH, storage.getParentUri());
|
||||||
intent.putExtra(EXTRA_PATH, storage.getUri());
|
intent.putExtra(EXTRA_PATH, storage.getUri());
|
||||||
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
|
intent.putExtra(EXTRA_STORAGE_TAG, storage.getTag());
|
||||||
|
intent.putExtra(EXTRA_SEGMENTS, segments);
|
||||||
|
|
||||||
context.startService(intent);
|
context.startService(intent);
|
||||||
}
|
}
|
||||||
|
@ -419,6 +426,7 @@ public class DownloadManagerService extends Service {
|
||||||
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
long nearLength = intent.getLongExtra(EXTRA_NEAR_LENGTH, 0);
|
||||||
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
String tag = intent.getStringExtra(EXTRA_STORAGE_TAG);
|
||||||
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
|
Parcelable[] parcelRecovery = intent.getParcelableArrayExtra(EXTRA_RECOVERY_INFO);
|
||||||
|
VideoSegment[] segments = (VideoSegment[]) intent.getSerializableExtra(EXTRA_SEGMENTS);
|
||||||
|
|
||||||
StoredFileHelper storage;
|
StoredFileHelper storage;
|
||||||
try {
|
try {
|
||||||
|
@ -443,6 +451,25 @@ public class DownloadManagerService extends Service {
|
||||||
mission.nearLength = nearLength;
|
mission.nearLength = nearLength;
|
||||||
mission.recoveryInfo = recovery;
|
mission.recoveryInfo = recovery;
|
||||||
|
|
||||||
|
if (segments != null && segments.length > 0) {
|
||||||
|
try {
|
||||||
|
final JsonStringWriter writer = JsonWriter.string()
|
||||||
|
.object()
|
||||||
|
.array("segments");
|
||||||
|
for (final VideoSegment segment : segments) {
|
||||||
|
writer.object()
|
||||||
|
.value("start", segment.startTime)
|
||||||
|
.value("end", segment.endTime)
|
||||||
|
.value("category", segment.category)
|
||||||
|
.end();
|
||||||
|
}
|
||||||
|
writer.end().end();
|
||||||
|
mission.segmentsJson = writer.done();
|
||||||
|
} catch (final Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (ps != null)
|
if (ps != null)
|
||||||
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
ps.setTemporalDir(DownloadManager.pickAvailableTemporalDir(this));
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
||||||
import android.app.NotificationManager;
|
import android.app.NotificationManager;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
import android.graphics.Color;
|
import android.graphics.Color;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -30,6 +31,7 @@ import androidx.core.app.NotificationCompat;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.content.FileProvider;
|
import androidx.core.content.FileProvider;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
import androidx.recyclerview.widget.DiffUtil;
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
import androidx.recyclerview.widget.RecyclerView.Adapter;
|
||||||
|
@ -37,7 +39,9 @@ import androidx.recyclerview.widget.RecyclerView.ViewHolder;
|
||||||
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.App;
|
||||||
import org.schabi.newpipe.BuildConfig;
|
import org.schabi.newpipe.BuildConfig;
|
||||||
|
import org.schabi.newpipe.LocalPlayerActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.NewPipe;
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
|
@ -122,8 +126,12 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
|
|
||||||
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
private final CompositeDisposable compositeDisposable = new CompositeDisposable();
|
||||||
|
|
||||||
|
private SharedPreferences mPrefs;
|
||||||
|
|
||||||
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
|
public MissionAdapter(Context context, @NonNull DownloadManager downloadManager, View emptyMessage, View root) {
|
||||||
mContext = context;
|
mContext = context;
|
||||||
|
mPrefs = PreferenceManager.getDefaultSharedPreferences(App.getApp());
|
||||||
|
|
||||||
mDownloadManager = downloadManager;
|
mDownloadManager = downloadManager;
|
||||||
|
|
||||||
mInflater = LayoutInflater.from(mContext);
|
mInflater = LayoutInflater.from(mContext);
|
||||||
|
@ -338,7 +346,25 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void viewWithFileProvider(Mission mission) {
|
private void open(Mission mission) {
|
||||||
|
if (checkInvalidFile(mission)) return;
|
||||||
|
|
||||||
|
String mimeType = resolveMimeType(mission);
|
||||||
|
|
||||||
|
if (BuildConfig.DEBUG)
|
||||||
|
Log.v(TAG, "Mime: " + mimeType + " package: " + BuildConfig.APPLICATION_ID + ".provider");
|
||||||
|
|
||||||
|
Uri uri = resolveShareableUri(mission);
|
||||||
|
|
||||||
|
Intent intent = new Intent(mContext, LocalPlayerActivity.class);
|
||||||
|
intent.setDataAndType(uri, mimeType);
|
||||||
|
intent.putExtra("segments", mission.segmentsJson);
|
||||||
|
intent.setFlags(FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
|
||||||
|
|
||||||
|
mContext.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void openExternally(Mission mission) {
|
||||||
if (checkInvalidFile(mission)) return;
|
if (checkInvalidFile(mission)) return;
|
||||||
|
|
||||||
String mimeType = resolveMimeType(mission);
|
String mimeType = resolveMimeType(mission);
|
||||||
|
@ -679,6 +705,9 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
applyChanges();
|
applyChanges();
|
||||||
checkMasterButtonsVisibility();
|
checkMasterButtonsVisibility();
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.open_externally:
|
||||||
|
openExternally(h.item.mission);
|
||||||
|
return true;
|
||||||
case R.id.md5:
|
case R.id.md5:
|
||||||
case R.id.sha1:
|
case R.id.sha1:
|
||||||
final NotificationManager notificationManager
|
final NotificationManager notificationManager
|
||||||
|
@ -896,8 +925,14 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
||||||
itemView.setHapticFeedbackEnabled(true);
|
itemView.setHapticFeedbackEnabled(true);
|
||||||
|
|
||||||
itemView.setOnClickListener(v -> {
|
itemView.setOnClickListener(v -> {
|
||||||
if (item.mission instanceof FinishedMission)
|
if (item.mission instanceof FinishedMission) {
|
||||||
viewWithFileProvider(item.mission);
|
if (mPrefs.getBoolean(mContext
|
||||||
|
.getString(R.string.enable_local_player_key), false)) {
|
||||||
|
open(item.mission);
|
||||||
|
} else {
|
||||||
|
openExternally(item.mission);
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
itemView.setOnLongClickListener(v -> {
|
itemView.setOnLongClickListener(v -> {
|
||||||
|
|
16
app/src/main/res/layout/activity_local_player.xml
Normal file
16
app/src/main/res/layout/activity_local_player.xml
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:background="@color/black"
|
||||||
|
tools:context=".LocalPlayerActivity">
|
||||||
|
<com.google.android.exoplayer2.ui.PlayerView
|
||||||
|
android:id="@+id/player_view"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
app:show_buffering="when_playing"
|
||||||
|
app:show_shuffle_button="true"/>
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
|
@ -29,6 +29,10 @@
|
||||||
android:id="@+id/delete"
|
android:id="@+id/delete"
|
||||||
android:title="@string/delete" />
|
android:title="@string/delete" />
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/open_externally"
|
||||||
|
android:title="@string/open_with" />
|
||||||
|
|
||||||
<item
|
<item
|
||||||
android:id="@+id/error_message_view"
|
android:id="@+id/error_message_view"
|
||||||
android:title="@string/show_error" />
|
android:title="@string/show_error" />
|
||||||
|
|
|
@ -364,6 +364,7 @@
|
||||||
<string name="sponsor_block_clear_whitelist_key" translatable="false">sponsor_block_clear_whitelist</string>
|
<string name="sponsor_block_clear_whitelist_key" translatable="false">sponsor_block_clear_whitelist</string>
|
||||||
|
|
||||||
<!-- Extras -->
|
<!-- Extras -->
|
||||||
|
<string name="enable_local_player_key" translatable="false">enable_local_player</string>
|
||||||
<string name="disable_tablet_ui_key" translatable="false">disable_tablet_ui</string>
|
<string name="disable_tablet_ui_key" translatable="false">disable_tablet_ui</string>
|
||||||
<string name="disable_tv_ui_key" translatable="false">disable_tv_ui</string>
|
<string name="disable_tv_ui_key" translatable="false">disable_tv_ui</string>
|
||||||
|
|
||||||
|
|
|
@ -360,6 +360,7 @@
|
||||||
<string name="checksum">Checksum</string>
|
<string name="checksum">Checksum</string>
|
||||||
<string name="dismiss">Dismiss</string>
|
<string name="dismiss">Dismiss</string>
|
||||||
<string name="rename">Rename</string>
|
<string name="rename">Rename</string>
|
||||||
|
<string name="open_with">Open With...</string>
|
||||||
<!-- Fragment -->
|
<!-- Fragment -->
|
||||||
<string name="add">New mission</string>
|
<string name="add">New mission</string>
|
||||||
<string name="finish">OK</string>
|
<string name="finish">OK</string>
|
||||||
|
@ -750,6 +751,8 @@
|
||||||
<string name="extras">Extras</string>
|
<string name="extras">Extras</string>
|
||||||
<string name="extras_todo_summary">Tweaks, workarounds, and other miscellaneous settings belong here.</string>
|
<string name="extras_todo_summary">Tweaks, workarounds, and other miscellaneous settings belong here.</string>
|
||||||
<string name="experimental_settings">Experimental Settings</string>
|
<string name="experimental_settings">Experimental Settings</string>
|
||||||
|
<string name="enable_local_player_title">Enable Local Player (alpha)</string>
|
||||||
|
<string name="enable_local_player_summary">Use a built-in player for local playback. This is still in early development so there will probably be a lot of issues, including conflicts with the existing player.</string>
|
||||||
<string name="disable_tablet_ui_title">Disable Tablet UI</string>
|
<string name="disable_tablet_ui_title">Disable Tablet UI</string>
|
||||||
<string name="disable_tablet_ui_summary">Ignore tablet layouts. This is intended for specific workarounds. You may need to restart the app to see the effects.</string>
|
<string name="disable_tablet_ui_summary">Ignore tablet layouts. This is intended for specific workarounds. You may need to restart the app to see the effects.</string>
|
||||||
<string name="disable_tv_ui_title">Disable TV UI</string>
|
<string name="disable_tv_ui_title">Disable TV UI</string>
|
||||||
|
|
|
@ -13,6 +13,13 @@
|
||||||
android:layout="@layout/settings_category_header_layout"
|
android:layout="@layout/settings_category_header_layout"
|
||||||
android:title="@string/experimental_settings">
|
android:title="@string/experimental_settings">
|
||||||
|
|
||||||
|
<SwitchPreference
|
||||||
|
app:iconSpaceReserved="false"
|
||||||
|
android:defaultValue="false"
|
||||||
|
android:key="@string/enable_local_player_key"
|
||||||
|
android:summary="@string/enable_local_player_summary"
|
||||||
|
android:title="@string/enable_local_player_title"/>
|
||||||
|
|
||||||
<SwitchPreference
|
<SwitchPreference
|
||||||
app:iconSpaceReserved="false"
|
app:iconSpaceReserved="false"
|
||||||
android:defaultValue="false"
|
android:defaultValue="false"
|
||||||
|
|
|
@ -7,7 +7,7 @@ buildscript {
|
||||||
google()
|
google()
|
||||||
}
|
}
|
||||||
dependencies {
|
dependencies {
|
||||||
classpath 'com.android.tools.build:gradle:4.1.1'
|
classpath 'com.android.tools.build:gradle:4.1.2'
|
||||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||||
|
|
||||||
// NOTE: Do not place your application dependencies here; they belong
|
// NOTE: Do not place your application dependencies here; they belong
|
||||||
|
|
Loading…
Reference in a new issue