mirror of
https://github.com/TeamNewPipe/NewPipe
synced 2026-02-10 20:20:16 +00:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d42058d99e | ||
|
|
a06034f189 | ||
|
|
3635b40daa | ||
|
|
81ee67b03b | ||
|
|
b2d4fdb0fb | ||
|
|
e7b5231708 | ||
|
|
d0edb9482d | ||
|
|
4282f78dd4 | ||
|
|
3150cae5f6 | ||
|
|
2c3da68329 | ||
|
|
9830c55563 | ||
|
|
ead12cec74 | ||
|
|
ae7078e5b6 | ||
|
|
93d3909e19 | ||
|
|
7ae7cb3b7e |
@@ -79,7 +79,7 @@ configure<ApplicationExtension> {
|
|||||||
resValue("string", "app_name", "NewPipe $suffix")
|
resValue("string", "app_name", "NewPipe $suffix")
|
||||||
}
|
}
|
||||||
isMinifyEnabled = true
|
isMinifyEnabled = true
|
||||||
isShrinkResources = false // disabled to fix F-Droid"s reproducible build
|
isShrinkResources = true
|
||||||
proguardFiles(
|
proguardFiles(
|
||||||
getDefaultProguardFile("proguard-android-optimize.txt"),
|
getDefaultProguardFile("proguard-android-optimize.txt"),
|
||||||
"proguard-rules.pro"
|
"proguard-rules.pro"
|
||||||
@@ -88,13 +88,7 @@ configure<ApplicationExtension> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
lint {
|
lint {
|
||||||
checkReleaseBuilds = false
|
lintConfig = file("lint.xml")
|
||||||
// Or, if you prefer, you can continue to check for errors in release builds,
|
|
||||||
// but continue the build even when errors are found:
|
|
||||||
abortOnError = false
|
|
||||||
// suppress false warning ("Resource IDs will be non-final in Android Gradle Plugin version
|
|
||||||
// 5.0, avoid using them in switch case statements"), which affects only library projects
|
|
||||||
disable += "NonConstantResourceId"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
compileOptions {
|
compileOptions {
|
||||||
|
|||||||
10
app/lint.xml
Normal file
10
app/lint.xml
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<!--
|
||||||
|
~ SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
~ SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
-->
|
||||||
|
<lint>
|
||||||
|
<issue id="MissingTranslation" severity="ignore" />
|
||||||
|
<issue id="MissingQuantity" severity="ignore" />
|
||||||
|
<issue id="ImpliedQuantity" severity="ignore" />
|
||||||
|
</lint>
|
||||||
50
app/src/main/java/org/schabi/newpipe/ExitActivity.java
Normal file
50
app/src/main/java/org/schabi/newpipe/ExitActivity.java
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
package org.schabi.newpipe;
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint;
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Copyright (C) Hans-Christoph Steiner 2016 <hans@eds.org>
|
||||||
|
* ExitActivity.java is part of NewPipe.
|
||||||
|
*
|
||||||
|
* NewPipe 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.
|
||||||
|
*
|
||||||
|
* NewPipe 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 NewPipe. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
public class ExitActivity extends Activity {
|
||||||
|
|
||||||
|
public static void exitAndRemoveFromRecentApps(final Activity activity) {
|
||||||
|
final Intent intent = new Intent(activity, ExitActivity.class);
|
||||||
|
|
||||||
|
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
|
| Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
||||||
|
| Intent.FLAG_ACTIVITY_CLEAR_TASK
|
||||||
|
| Intent.FLAG_ACTIVITY_NO_ANIMATION);
|
||||||
|
|
||||||
|
activity.startActivity(intent);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("NewApi")
|
||||||
|
@Override
|
||||||
|
protected void onCreate(final Bundle savedInstanceState) {
|
||||||
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
|
finishAndRemoveTask();
|
||||||
|
|
||||||
|
NavigationHelper.restartApp(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2016-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.content.Intent
|
|
||||||
import android.os.Bundle
|
|
||||||
import org.schabi.newpipe.util.NavigationHelper
|
|
||||||
|
|
||||||
class ExitActivity : Activity() {
|
|
||||||
@SuppressLint("NewApi")
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
finishAndRemoveTask()
|
|
||||||
NavigationHelper.restartApp(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic
|
|
||||||
fun exitAndRemoveFromRecentApps(activity: Activity) {
|
|
||||||
val intent = Intent(activity, ExitActivity::class.java)
|
|
||||||
intent.addFlags(
|
|
||||||
Intent.FLAG_ACTIVITY_NEW_TASK
|
|
||||||
or Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS
|
|
||||||
or Intent.FLAG_ACTIVITY_CLEAR_TASK
|
|
||||||
or Intent.FLAG_ACTIVITY_NO_ANIMATION
|
|
||||||
)
|
|
||||||
|
|
||||||
activity.startActivity(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -309,25 +309,21 @@ public class MainActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private boolean drawerItemSelected(final MenuItem item) {
|
private boolean drawerItemSelected(final MenuItem item) {
|
||||||
switch (item.getGroupId()) {
|
final int groupId = item.getGroupId();
|
||||||
case R.id.menu_services_group:
|
if (groupId == R.id.menu_services_group) {
|
||||||
changeService(item);
|
changeService(item);
|
||||||
break;
|
} else if (groupId == R.id.menu_tabs_group) {
|
||||||
case R.id.menu_tabs_group:
|
tabSelected(item);
|
||||||
tabSelected(item);
|
} else if (groupId == R.id.menu_kiosks_group) {
|
||||||
break;
|
try {
|
||||||
case R.id.menu_kiosks_group:
|
kioskSelected(item);
|
||||||
try {
|
} catch (final Exception e) {
|
||||||
kioskSelected(item);
|
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
|
||||||
} catch (final Exception e) {
|
}
|
||||||
ErrorUtil.showUiErrorSnackbar(this, "Selecting drawer kiosk", e);
|
} else if (groupId == R.id.menu_options_about_group) {
|
||||||
}
|
optionsAboutSelected(item);
|
||||||
break;
|
} else {
|
||||||
case R.id.menu_options_about_group:
|
return false;
|
||||||
optionsAboutSelected(item);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mainBinding.getRoot().closeDrawers();
|
mainBinding.getRoot().closeDrawers();
|
||||||
|
|||||||
@@ -82,7 +82,9 @@ class NewVersionWorker(
|
|||||||
)
|
)
|
||||||
|
|
||||||
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
val notificationManager = NotificationManagerCompat.from(applicationContext)
|
||||||
notificationManager.notify(2000, notificationBuilder.build())
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(2000, notificationBuilder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Throws(IOException::class, ReCaptchaException::class)
|
@Throws(IOException::class, ReCaptchaException::class)
|
||||||
|
|||||||
@@ -41,50 +41,50 @@ public final class QueueItemMenuUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
popupMenu.setOnMenuItemClickListener(menuItem -> {
|
||||||
switch (menuItem.getItemId()) {
|
final int itemId = menuItem.getItemId();
|
||||||
case R.id.menu_item_remove:
|
if (itemId == R.id.menu_item_remove) {
|
||||||
final int index = playQueue.indexOf(item);
|
final int index = playQueue.indexOf(item);
|
||||||
playQueue.remove(index);
|
playQueue.remove(index);
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_details:
|
} else if (itemId == R.id.menu_item_details) {
|
||||||
// playQueue is null since we don't want any queue change
|
// playQueue is null since we don't want any queue change
|
||||||
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
NavigationHelper.openVideoDetail(context, item.getServiceId(),
|
||||||
item.getUrl(), item.getTitle(), null,
|
item.getUrl(), item.getTitle(), null,
|
||||||
false);
|
false);
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_append_playlist:
|
} else if (itemId == R.id.menu_item_append_playlist) {
|
||||||
PlaylistDialog.createCorrespondingDialog(
|
PlaylistDialog.createCorrespondingDialog(
|
||||||
context,
|
context,
|
||||||
List.of(new StreamEntity(item)),
|
List.of(new StreamEntity(item)),
|
||||||
dialog -> dialog.show(
|
dialog -> dialog.show(
|
||||||
fragmentManager,
|
fragmentManager,
|
||||||
"QueueItemMenuUtil@append_playlist"
|
"QueueItemMenuUtil@append_playlist"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_channel_details:
|
} else if (itemId == R.id.menu_item_channel_details) {
|
||||||
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
SparseItemUtil.fetchUploaderUrlIfSparse(context, item.getServiceId(),
|
||||||
item.getUrl(), item.getUploaderUrl(),
|
item.getUrl(), item.getUploaderUrl(),
|
||||||
// An intent must be used here.
|
// An intent must be used here.
|
||||||
// Opening with FragmentManager transactions is not working,
|
// Opening with FragmentManager transactions is not working,
|
||||||
// as PlayQueueActivity doesn't use fragments.
|
// as PlayQueueActivity doesn't use fragments.
|
||||||
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
uploaderUrl -> NavigationHelper.openChannelFragmentUsingIntent(
|
||||||
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
context, item.getServiceId(), uploaderUrl, item.getUploader()
|
||||||
));
|
));
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share:
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
shareText(context, item.getTitle(), item.getUrl(),
|
shareText(context, item.getTitle(), item.getUrl(),
|
||||||
item.getThumbnails());
|
item.getThumbnails());
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_download:
|
} else if (itemId == R.id.menu_item_download) {
|
||||||
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
fetchStreamInfoAndSaveToDatabase(context, item.getServiceId(), item.getUrl(),
|
||||||
info -> {
|
info -> {
|
||||||
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
final DownloadDialog downloadDialog = new DownloadDialog(context,
|
||||||
info);
|
info);
|
||||||
downloadDialog.show(fragmentManager, "downloadDialog");
|
downloadDialog.show(fragmentManager, "downloadDialog");
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -62,7 +62,11 @@ data class PlaylistRemoteEntity(
|
|||||||
orderingName = playlistInfo.name,
|
orderingName = playlistInfo.name,
|
||||||
url = playlistInfo.url,
|
url = playlistInfo.url,
|
||||||
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
thumbnailUrl = ImageStrategy.imageListToDbUrl(
|
||||||
playlistInfo.thumbnails.ifEmpty { playlistInfo.uploaderAvatars }
|
if (playlistInfo.thumbnails.isEmpty()) {
|
||||||
|
playlistInfo.uploaderAvatars
|
||||||
|
} else {
|
||||||
|
playlistInfo.thumbnails
|
||||||
|
}
|
||||||
),
|
),
|
||||||
uploader = playlistInfo.uploaderName,
|
uploader = playlistInfo.uploaderName,
|
||||||
streamCount = playlistInfo.streamCount
|
streamCount = playlistInfo.streamCount
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import android.os.IBinder;
|
|||||||
import android.provider.Settings;
|
import android.provider.Settings;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.AdapterView;
|
import android.widget.AdapterView;
|
||||||
@@ -31,7 +32,6 @@ import androidx.annotation.NonNull;
|
|||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
import androidx.appcompat.app.AlertDialog;
|
import androidx.appcompat.app.AlertDialog;
|
||||||
import androidx.appcompat.view.menu.ActionMenuItemView;
|
|
||||||
import androidx.appcompat.widget.Toolbar;
|
import androidx.appcompat.widget.Toolbar;
|
||||||
import androidx.collection.SparseArrayCompat;
|
import androidx.collection.SparseArrayCompat;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
@@ -113,7 +113,7 @@ public class DownloadDialog extends DialogFragment
|
|||||||
private StoredDirectoryHelper mainStorageAudio = null;
|
private StoredDirectoryHelper mainStorageAudio = null;
|
||||||
private StoredDirectoryHelper mainStorageVideo = null;
|
private StoredDirectoryHelper mainStorageVideo = null;
|
||||||
private DownloadManager downloadManager = null;
|
private DownloadManager downloadManager = null;
|
||||||
private ActionMenuItemView okButton = null;
|
private MenuItem okButton = null;
|
||||||
private Context context = null;
|
private Context context = null;
|
||||||
private boolean askForSavePath;
|
private boolean askForSavePath;
|
||||||
|
|
||||||
@@ -558,17 +558,13 @@ public class DownloadDialog extends DialogFragment
|
|||||||
}
|
}
|
||||||
boolean flag = true;
|
boolean flag = true;
|
||||||
|
|
||||||
switch (checkedId) {
|
if (checkedId == R.id.audio_button) {
|
||||||
case R.id.audio_button:
|
setupAudioSpinner();
|
||||||
setupAudioSpinner();
|
} else if (checkedId == R.id.video_button) {
|
||||||
break;
|
setupVideoSpinner();
|
||||||
case R.id.video_button:
|
} else if (checkedId == R.id.subtitle_button) {
|
||||||
setupVideoSpinner();
|
setupSubtitleSpinner();
|
||||||
break;
|
flag = false;
|
||||||
case R.id.subtitle_button:
|
|
||||||
setupSubtitleSpinner();
|
|
||||||
flag = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
dialogBinding.threads.setEnabled(flag);
|
dialogBinding.threads.setEnabled(flag);
|
||||||
@@ -585,29 +581,26 @@ public class DownloadDialog extends DialogFragment
|
|||||||
+ "position = [" + position + "], id = [" + id + "]");
|
+ "position = [" + position + "], id = [" + id + "]");
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (parent.getId()) {
|
final int parentId = parent.getId();
|
||||||
case R.id.quality_spinner:
|
if (parentId == R.id.quality_spinner) {
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int checkedRadioButtonId = dialogBinding.videoAudioGroup
|
||||||
case R.id.video_button:
|
.getCheckedRadioButtonId();
|
||||||
selectedVideoIndex = position;
|
if (checkedRadioButtonId == R.id.video_button) {
|
||||||
onVideoStreamSelected();
|
selectedVideoIndex = position;
|
||||||
break;
|
onVideoStreamSelected();
|
||||||
case R.id.subtitle_button:
|
} else if (checkedRadioButtonId == R.id.subtitle_button) {
|
||||||
selectedSubtitleIndex = position;
|
selectedSubtitleIndex = position;
|
||||||
break;
|
}
|
||||||
}
|
onItemSelectedSetFileName();
|
||||||
onItemSelectedSetFileName();
|
} else if (parentId == R.id.audio_track_spinner) {
|
||||||
break;
|
final boolean trackChanged = selectedAudioTrackIndex != position;
|
||||||
case R.id.audio_track_spinner:
|
selectedAudioTrackIndex = position;
|
||||||
final boolean trackChanged = selectedAudioTrackIndex != position;
|
if (trackChanged) {
|
||||||
selectedAudioTrackIndex = position;
|
updateSecondaryStreams();
|
||||||
if (trackChanged) {
|
fetchStreamsSize();
|
||||||
updateSecondaryStreams();
|
}
|
||||||
fetchStreamsSize();
|
} else if (parentId == R.id.audio_stream_spinner) {
|
||||||
}
|
selectedAudioIndex = position;
|
||||||
break;
|
|
||||||
case R.id.audio_stream_spinner:
|
|
||||||
selectedAudioIndex = position;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -622,23 +615,20 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
|
|| prevFileName.startsWith(getString(R.string.caption_file_name, fileName, ""))) {
|
||||||
// only update the file name field if it was not edited by the user
|
// only update the file name field if it was not edited by the user
|
||||||
|
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int radioButtonId = dialogBinding.videoAudioGroup
|
||||||
case R.id.audio_button:
|
.getCheckedRadioButtonId();
|
||||||
case R.id.video_button:
|
if (radioButtonId == R.id.audio_button || radioButtonId == R.id.video_button) {
|
||||||
if (!prevFileName.equals(fileName)) {
|
if (!prevFileName.equals(fileName)) {
|
||||||
// since the user might have switched between audio and video, the correct
|
// since the user might have switched between audio and video, the correct
|
||||||
// text might already be in place, so avoid resetting the cursor position
|
// text might already be in place, so avoid resetting the cursor position
|
||||||
dialogBinding.fileName.setText(fileName);
|
dialogBinding.fileName.setText(fileName);
|
||||||
}
|
}
|
||||||
break;
|
} else if (radioButtonId == R.id.subtitle_button) {
|
||||||
|
final String setSubtitleLanguageCode = subtitleStreamsAdapter
|
||||||
case R.id.subtitle_button:
|
.getItem(selectedSubtitleIndex).getLanguageTag();
|
||||||
final String setSubtitleLanguageCode = subtitleStreamsAdapter
|
// this will reset the cursor position, which is bad UX, but it can't be avoided
|
||||||
.getItem(selectedSubtitleIndex).getLanguageTag();
|
dialogBinding.fileName.setText(getString(
|
||||||
// this will reset the cursor position, which is bad UX, but it can't be avoided
|
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
|
||||||
dialogBinding.fileName.setText(getString(
|
|
||||||
R.string.caption_file_name, fileName, setSubtitleLanguageCode));
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -770,47 +760,44 @@ public class DownloadDialog extends DialogFragment
|
|||||||
|
|
||||||
filenameTmp = getNameEditText().concat(".");
|
filenameTmp = getNameEditText().concat(".");
|
||||||
|
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
|
||||||
case R.id.audio_button:
|
if (checkedRadioButtonId == R.id.audio_button) {
|
||||||
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
selectedMediaType = getString(R.string.last_download_type_audio_key);
|
||||||
mainStorage = mainStorageAudio;
|
mainStorage = mainStorageAudio;
|
||||||
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
format = audioStreamsAdapter.getItem(selectedAudioIndex).getFormat();
|
||||||
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
size = getWrappedAudioStreams().getSizeInBytes(selectedAudioIndex);
|
||||||
if (format == MediaFormat.WEBMA_OPUS) {
|
if (format == MediaFormat.WEBMA_OPUS) {
|
||||||
mimeTmp = "audio/ogg";
|
mimeTmp = "audio/ogg";
|
||||||
filenameTmp += "opus";
|
filenameTmp += "opus";
|
||||||
} else if (format != null) {
|
} else if (format != null) {
|
||||||
mimeTmp = format.mimeType;
|
mimeTmp = format.mimeType;
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
} else if (checkedRadioButtonId == R.id.video_button) {
|
||||||
case R.id.video_button:
|
selectedMediaType = getString(R.string.last_download_type_video_key);
|
||||||
selectedMediaType = getString(R.string.last_download_type_video_key);
|
mainStorage = mainStorageVideo;
|
||||||
mainStorage = mainStorageVideo;
|
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
||||||
format = videoStreamsAdapter.getItem(selectedVideoIndex).getFormat();
|
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
||||||
size = wrappedVideoStreams.getSizeInBytes(selectedVideoIndex);
|
if (format != null) {
|
||||||
if (format != null) {
|
mimeTmp = format.mimeType;
|
||||||
mimeTmp = format.mimeType;
|
filenameTmp += format.getSuffix();
|
||||||
filenameTmp += format.getSuffix();
|
}
|
||||||
}
|
} else if (checkedRadioButtonId == R.id.subtitle_button) {
|
||||||
break;
|
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
||||||
case R.id.subtitle_button:
|
mainStorage = mainStorageVideo; // subtitle & video files go together
|
||||||
selectedMediaType = getString(R.string.last_download_type_subtitle_key);
|
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
||||||
mainStorage = mainStorageVideo; // subtitle & video files go together
|
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
||||||
format = subtitleStreamsAdapter.getItem(selectedSubtitleIndex).getFormat();
|
if (format != null) {
|
||||||
size = wrappedSubtitleStreams.getSizeInBytes(selectedSubtitleIndex);
|
mimeTmp = format.mimeType;
|
||||||
if (format != null) {
|
}
|
||||||
mimeTmp = format.mimeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (format == MediaFormat.TTML) {
|
if (format == MediaFormat.TTML) {
|
||||||
filenameTmp += MediaFormat.SRT.getSuffix();
|
filenameTmp += MediaFormat.SRT.getSuffix();
|
||||||
} else if (format != null) {
|
} else if (format != null) {
|
||||||
filenameTmp += format.getSuffix();
|
filenameTmp += format.getSuffix();
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
default:
|
throw new RuntimeException("No stream selected");
|
||||||
throw new RuntimeException("No stream selected");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!askForSavePath && (mainStorage == null
|
if (!askForSavePath && (mainStorage == null
|
||||||
@@ -1057,59 +1044,56 @@ public class DownloadDialog extends DialogFragment
|
|||||||
long nearLength = 0;
|
long nearLength = 0;
|
||||||
|
|
||||||
// more download logic: select muxer, subtitle converter, etc.
|
// more download logic: select muxer, subtitle converter, etc.
|
||||||
switch (dialogBinding.videoAudioGroup.getCheckedRadioButtonId()) {
|
final int checkedRadioButtonId = dialogBinding.videoAudioGroup.getCheckedRadioButtonId();
|
||||||
case R.id.audio_button:
|
if (checkedRadioButtonId == R.id.audio_button) {
|
||||||
kind = 'a';
|
kind = 'a';
|
||||||
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
selectedStream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
if (selectedStream.getFormat() == MediaFormat.M4A) {
|
||||||
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
psName = Postprocessing.ALGORITHM_M4A_NO_DASH;
|
||||||
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
} else if (selectedStream.getFormat() == MediaFormat.WEBMA_OPUS) {
|
||||||
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
psName = Postprocessing.ALGORITHM_OGG_FROM_WEBM_DEMUXER;
|
||||||
|
}
|
||||||
|
} else if (checkedRadioButtonId == R.id.video_button) {
|
||||||
|
kind = 'v';
|
||||||
|
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||||
|
|
||||||
|
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
||||||
|
.getAllSecondary()
|
||||||
|
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
||||||
|
|
||||||
|
if (secondary != null) {
|
||||||
|
secondaryStream = secondary.getStream();
|
||||||
|
|
||||||
|
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
||||||
|
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
||||||
|
} else {
|
||||||
|
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
case R.id.video_button:
|
|
||||||
kind = 'v';
|
|
||||||
selectedStream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
|
||||||
|
|
||||||
final SecondaryStreamHelper<AudioStream> secondary = videoStreamsAdapter
|
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
||||||
.getAllSecondary()
|
(VideoStream) selectedStream);
|
||||||
.get(wrappedVideoStreams.getStreamsList().indexOf(selectedStream));
|
|
||||||
|
|
||||||
if (secondary != null) {
|
// set nearLength, only, if both sizes are fetched or known. This probably
|
||||||
secondaryStream = secondary.getStream();
|
// does not work on slow networks but is later updated in the downloader
|
||||||
|
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
||||||
if (selectedStream.getFormat() == MediaFormat.MPEG_4) {
|
nearLength = secondary.getSizeInBytes() + videoSize;
|
||||||
psName = Postprocessing.ALGORITHM_MP4_FROM_DASH_MUXER;
|
|
||||||
} else {
|
|
||||||
psName = Postprocessing.ALGORITHM_WEBM_MUXER;
|
|
||||||
}
|
|
||||||
|
|
||||||
final long videoSize = wrappedVideoStreams.getSizeInBytes(
|
|
||||||
(VideoStream) selectedStream);
|
|
||||||
|
|
||||||
// set nearLength, only, if both sizes are fetched or known. This probably
|
|
||||||
// does not work on slow networks but is later updated in the downloader
|
|
||||||
if (secondary.getSizeInBytes() > 0 && videoSize > 0) {
|
|
||||||
nearLength = secondary.getSizeInBytes() + videoSize;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
break;
|
}
|
||||||
case R.id.subtitle_button:
|
} else if (checkedRadioButtonId == R.id.subtitle_button) {
|
||||||
threads = 1; // use unique thread for subtitles due small file size
|
threads = 1; // use unique thread for subtitles due small file size
|
||||||
kind = 's';
|
kind = 's';
|
||||||
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
selectedStream = subtitleStreamsAdapter.getItem(selectedSubtitleIndex);
|
||||||
|
|
||||||
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
if (selectedStream.getFormat() == MediaFormat.TTML) {
|
||||||
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
psName = Postprocessing.ALGORITHM_TTML_CONVERTER;
|
||||||
psArgs = new String[] {
|
psArgs = new String[]{
|
||||||
selectedStream.getFormat().getSuffix(),
|
selectedStream.getFormat().getSuffix(),
|
||||||
"false" // ignore empty frames
|
"false" // ignore empty frames
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
break;
|
} else {
|
||||||
default:
|
return;
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (secondaryStream == null) {
|
if (secondaryStream == null) {
|
||||||
|
|||||||
@@ -133,17 +133,16 @@ public class ErrorActivity extends AppCompatActivity {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
case android.R.id.home:
|
if (itemId == android.R.id.home) {
|
||||||
onBackPressed();
|
onBackPressed();
|
||||||
return true;
|
return true;
|
||||||
case R.id.menu_item_share_error:
|
} else if (itemId == R.id.menu_item_share_error) {
|
||||||
ShareUtils.shareText(getApplicationContext(),
|
ShareUtils.shareText(getApplicationContext(),
|
||||||
getString(R.string.error_report_title), buildJson());
|
getString(R.string.error_report_title), buildJson());
|
||||||
return true;
|
return true;
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
private void openPrivacyPolicyDialog(final Context context, final String action) {
|
||||||
|
|||||||
@@ -134,8 +134,11 @@ class ErrorUtil {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
NotificationManagerCompat.from(context)
|
val notificationManager = NotificationManagerCompat.from(context)
|
||||||
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager
|
||||||
|
.notify(ERROR_REPORT_NOTIFICATION_ID, notificationBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
ContextCompat.getMainExecutor(context).execute {
|
ContextCompat.getMainExecutor(context).execute {
|
||||||
// since the notification is silent, also show a toast, otherwise the user is confused
|
// since the notification is silent, also show a toast, otherwise the user is confused
|
||||||
|
|||||||
@@ -126,6 +126,7 @@ public class ReCaptchaActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@SuppressLint("MissingSuperCall")
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
saveCookiesAndFinish();
|
saveCookiesAndFinish();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -160,34 +160,29 @@ public class ChannelFragment extends BaseStateFragment<ChannelInfo>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
public boolean onMenuItemSelected(@NonNull final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
case R.id.menu_item_notify:
|
if (itemId == R.id.menu_item_notify) {
|
||||||
final boolean value = !item.isChecked();
|
final boolean value = !item.isChecked();
|
||||||
item.setEnabled(false);
|
item.setEnabled(false);
|
||||||
setNotify(value);
|
setNotify(value);
|
||||||
break;
|
} else if (itemId == R.id.action_settings) {
|
||||||
case R.id.action_settings:
|
NavigationHelper.openSettings(requireContext());
|
||||||
NavigationHelper.openSettings(requireContext());
|
} else if (itemId == R.id.menu_item_rss) {
|
||||||
break;
|
if (currentInfo != null) {
|
||||||
case R.id.menu_item_rss:
|
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
||||||
if (currentInfo != null) {
|
}
|
||||||
ShareUtils.openUrlInApp(requireContext(), currentInfo.getFeedUrl());
|
} else if (itemId == R.id.menu_item_openInBrowser) {
|
||||||
}
|
if (currentInfo != null) {
|
||||||
break;
|
ShareUtils.openUrlInBrowser(requireContext(),
|
||||||
case R.id.menu_item_openInBrowser:
|
currentInfo.getOriginalUrl());
|
||||||
if (currentInfo != null) {
|
}
|
||||||
ShareUtils.openUrlInBrowser(requireContext(),
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
currentInfo.getOriginalUrl());
|
if (currentInfo != null) {
|
||||||
}
|
ShareUtils.shareText(requireContext(), name,
|
||||||
break;
|
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
||||||
case R.id.menu_item_share:
|
}
|
||||||
if (currentInfo != null) {
|
} else {
|
||||||
ShareUtils.shareText(requireContext(), name,
|
return false;
|
||||||
currentInfo.getOriginalUrl(), currentInfo.getAvatars());
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -232,35 +232,30 @@ public class PlaylistFragment extends BaseListInfoFragment<StreamInfoItem, Playl
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
case R.id.action_settings:
|
if (itemId == R.id.action_settings) {
|
||||||
NavigationHelper.openSettings(requireContext());
|
NavigationHelper.openSettings(requireContext());
|
||||||
break;
|
} else if (itemId == R.id.menu_item_openInBrowser) {
|
||||||
case R.id.menu_item_openInBrowser:
|
ShareUtils.openUrlInBrowser(requireContext(), url);
|
||||||
ShareUtils.openUrlInBrowser(requireContext(), url);
|
} else if (itemId == R.id.menu_item_share) {
|
||||||
break;
|
ShareUtils.shareText(requireContext(), name, url,
|
||||||
case R.id.menu_item_share:
|
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
||||||
ShareUtils.shareText(requireContext(), name, url,
|
} else if (itemId == R.id.menu_item_bookmark) {
|
||||||
currentInfo == null ? List.of() : currentInfo.getThumbnails());
|
onBookmarkClicked();
|
||||||
break;
|
} else if (itemId == R.id.menu_item_append_playlist) {
|
||||||
case R.id.menu_item_bookmark:
|
if (currentInfo != null) {
|
||||||
onBookmarkClicked();
|
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
||||||
break;
|
getContext(),
|
||||||
case R.id.menu_item_append_playlist:
|
getPlayQueue()
|
||||||
if (currentInfo != null) {
|
.getStreams()
|
||||||
disposables.add(PlaylistDialog.createCorrespondingDialog(
|
.stream()
|
||||||
getContext(),
|
.map(StreamEntity::new)
|
||||||
getPlayQueue()
|
.collect(Collectors.toList()),
|
||||||
.getStreams()
|
dialog -> dialog.show(getFM(), TAG)
|
||||||
.stream()
|
));
|
||||||
.map(StreamEntity::new)
|
}
|
||||||
.collect(Collectors.toList()),
|
} else {
|
||||||
dialog -> dialog.show(getFM(), TAG)
|
return super.onOptionsItemSelected(item);
|
||||||
));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -255,7 +255,7 @@ class FeedFragment : BaseStateFragment<FeedState>() {
|
|||||||
viewModel.getShowFutureItemsFromPreferences()
|
viewModel.getShowFutureItemsFromPreferences()
|
||||||
)
|
)
|
||||||
|
|
||||||
AlertDialog.Builder(context!!)
|
AlertDialog.Builder(requireContext())
|
||||||
.setTitle(R.string.feed_hide_streams_title)
|
.setTitle(R.string.feed_hide_streams_title)
|
||||||
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
.setMultiChoiceItems(dialogItems, checkedDialogItems) { _, which, isChecked ->
|
||||||
checkedDialogItems[which] = isChecked
|
checkedDialogItems[which] = isChecked
|
||||||
|
|||||||
@@ -129,7 +129,8 @@ class FeedViewModel(
|
|||||||
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
fun setSaveShowPlayedItems(showPlayedItems: Boolean) {
|
||||||
this.showPlayedItems.onNext(showPlayedItems)
|
this.showPlayedItems.onNext(showPlayedItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
this.putBoolean(application.getString(R.string.feed_show_watched_items_key), showPlayedItems)
|
||||||
|
this.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -138,7 +139,8 @@ class FeedViewModel(
|
|||||||
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
fun setSaveShowPartiallyPlayedItems(showPartiallyPlayedItems: Boolean) {
|
||||||
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
this.showPartiallyPlayedItems.onNext(showPartiallyPlayedItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
this.putBoolean(application.getString(R.string.feed_show_partially_watched_items_key), showPartiallyPlayedItems)
|
||||||
|
this.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,7 +149,8 @@ class FeedViewModel(
|
|||||||
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
fun setSaveShowFutureItems(showFutureItems: Boolean) {
|
||||||
this.showFutureItems.onNext(showFutureItems)
|
this.showFutureItems.onNext(showFutureItems)
|
||||||
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
PreferenceManager.getDefaultSharedPreferences(application).edit {
|
||||||
putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
this.putBoolean(application.getString(R.string.feed_show_future_items_key), showFutureItems)
|
||||||
|
this.apply()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -92,8 +92,10 @@ class NotificationHelper(val context: Context) {
|
|||||||
// Show individual stream notifications, set channel icon only if there is actually
|
// Show individual stream notifications, set channel icon only if there is actually
|
||||||
// one
|
// one
|
||||||
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
|
showStreamNotifications(newStreams, data.serviceId, data.url, bitmap)
|
||||||
// Show summary notification
|
// Show summary notification if enabled
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
if (manager.areNotificationsEnabled()) {
|
||||||
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
}
|
||||||
|
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
}
|
}
|
||||||
@@ -101,8 +103,10 @@ class NotificationHelper(val context: Context) {
|
|||||||
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
override fun onBitmapFailed(e: Exception, errorDrawable: Drawable) {
|
||||||
// Show individual stream notifications
|
// Show individual stream notifications
|
||||||
showStreamNotifications(newStreams, data.serviceId, data.url, null)
|
showStreamNotifications(newStreams, data.serviceId, data.url, null)
|
||||||
// Show summary notification
|
// Show summary notification if enabled
|
||||||
manager.notify(data.pseudoId, summaryBuilder.build())
|
if (manager.areNotificationsEnabled()) {
|
||||||
|
manager.notify(data.pseudoId, summaryBuilder.build())
|
||||||
|
}
|
||||||
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
iconLoadingTargets.remove(this) // allow it to be garbage-collected
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -126,7 +130,9 @@ class NotificationHelper(val context: Context) {
|
|||||||
) {
|
) {
|
||||||
for (stream in newStreams) {
|
for (stream in newStreams) {
|
||||||
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
|
val notification = createStreamNotification(stream, serviceId, channelUrl, channelIcon)
|
||||||
manager.notify(stream.url.hashCode(), notification)
|
if (manager.areNotificationsEnabled()) {
|
||||||
|
manager.notify(stream.url.hashCode(), notification)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -185,7 +185,9 @@ class FeedLoadService : Service() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// /////////////////////////////////////////////////////////////////////////
|
// /////////////////////////////////////////////////////////////////////////
|
||||||
|
|||||||
@@ -327,7 +327,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
groupIcon = feedGroupEntity?.icon
|
groupIcon = feedGroupEntity?.icon
|
||||||
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
groupSortOrder = feedGroupEntity?.sortOrder ?: -1
|
||||||
|
|
||||||
val feedGroupIcon = selectedIcon ?: icon
|
val feedGroupIcon = if (selectedIcon == null) icon else selectedIcon!!
|
||||||
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
|
feedGroupCreateBinding.iconPreview.setImageResource(feedGroupIcon.getDrawableRes())
|
||||||
|
|
||||||
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
if (feedGroupCreateBinding.groupNameInput.text.isNullOrBlank()) {
|
||||||
@@ -506,7 +506,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
private fun hideKeyboardSearch() {
|
private fun hideKeyboardSearch() {
|
||||||
inputMethodManager.hideSoftInputFromWindow(
|
inputMethodManager.hideSoftInputFromWindow(
|
||||||
searchLayoutBinding.toolbarSearchEditText.windowToken,
|
searchLayoutBinding.toolbarSearchEditText.windowToken,
|
||||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
InputMethodManager.HIDE_NOT_ALWAYS
|
||||||
)
|
)
|
||||||
searchLayoutBinding.toolbarSearchEditText.clearFocus()
|
searchLayoutBinding.toolbarSearchEditText.clearFocus()
|
||||||
}
|
}
|
||||||
@@ -523,7 +523,7 @@ class FeedGroupDialog : DialogFragment(), BackPressable {
|
|||||||
private fun hideKeyboard() {
|
private fun hideKeyboard() {
|
||||||
inputMethodManager.hideSoftInputFromWindow(
|
inputMethodManager.hideSoftInputFromWindow(
|
||||||
feedGroupCreateBinding.groupNameInput.windowToken,
|
feedGroupCreateBinding.groupNameInput.windowToken,
|
||||||
InputMethodManager.RESULT_UNCHANGED_SHOWN
|
InputMethodManager.HIDE_NOT_ALWAYS
|
||||||
)
|
)
|
||||||
feedGroupCreateBinding.groupNameInput.clearFocus()
|
feedGroupCreateBinding.groupNameInput.clearFocus()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,7 +144,9 @@ public abstract class BaseImportExportService extends Service {
|
|||||||
notificationBuilder.setContentText(text);
|
notificationBuilder.setContentText(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void stopService() {
|
protected void stopService() {
|
||||||
@@ -174,7 +176,10 @@ public abstract class BaseImportExportService extends Service {
|
|||||||
.setContentTitle(title)
|
.setContentTitle(title)
|
||||||
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
|
.setStyle(new NotificationCompat.BigTextStyle().bigText(textOrEmpty))
|
||||||
.setContentText(textOrEmpty);
|
.setContentText(textOrEmpty);
|
||||||
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
|
||||||
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(getNotificationId(), notificationBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected NotificationCompat.Builder createNotification() {
|
protected NotificationCompat.Builder createNotification() {
|
||||||
|
|||||||
@@ -127,39 +127,39 @@ public final class PlayQueueActivity extends AppCompatActivity
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(final MenuItem item) {
|
public boolean onOptionsItemSelected(final MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
final int itemId = item.getItemId();
|
||||||
case android.R.id.home:
|
if (itemId == android.R.id.home) {
|
||||||
finish();
|
finish();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_settings:
|
} else if (itemId == R.id.action_settings) {
|
||||||
NavigationHelper.openSettings(this);
|
NavigationHelper.openSettings(this);
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_append_playlist:
|
} else if (itemId == R.id.action_append_playlist) {
|
||||||
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
|
PlaylistDialog.showForPlayQueue(player, getSupportFragmentManager());
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_playback_speed:
|
} else if (itemId == R.id.action_playback_speed) {
|
||||||
openPlaybackParameterDialog();
|
openPlaybackParameterDialog();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_mute:
|
} else if (itemId == R.id.action_mute) {
|
||||||
player.toggleMute();
|
player.toggleMute();
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_system_audio:
|
} else if (itemId == R.id.action_system_audio) {
|
||||||
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
startActivity(new Intent(Settings.ACTION_SOUND_SETTINGS));
|
||||||
return true;
|
return true;
|
||||||
case R.id.action_switch_main:
|
} else if (itemId == R.id.action_switch_main) {
|
||||||
|
this.player.setRecovery();
|
||||||
|
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
|
||||||
|
return true;
|
||||||
|
} else if (itemId == R.id.action_switch_popup) {
|
||||||
|
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
|
||||||
this.player.setRecovery();
|
this.player.setRecovery();
|
||||||
NavigationHelper.playOnMainPlayer(this, player.getPlayQueue(), true);
|
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
|
||||||
return true;
|
}
|
||||||
case R.id.action_switch_popup:
|
return true;
|
||||||
if (PermissionHelper.isPopupEnabledElseAsk(this)) {
|
} else if (itemId == R.id.action_switch_background) {
|
||||||
this.player.setRecovery();
|
this.player.setRecovery();
|
||||||
NavigationHelper.playOnPopupPlayer(this, player.getPlayQueue(), true);
|
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
||||||
}
|
return true;
|
||||||
return true;
|
|
||||||
case R.id.action_switch_background:
|
|
||||||
this.player.setRecovery();
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(this, player.getPlayQueue(), true);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {
|
if (item.getGroupId() == MENU_ID_AUDIO_TRACK) {
|
||||||
|
|||||||
@@ -72,7 +72,9 @@ public final class NotificationUtil {
|
|||||||
notificationBuilder = createNotification();
|
notificationBuilder = createNotification();
|
||||||
}
|
}
|
||||||
updateNotification();
|
updateNotification();
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public synchronized void updateThumbnail() {
|
public synchronized void updateThumbnail() {
|
||||||
@@ -84,7 +86,9 @@ public final class NotificationUtil {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLargeIcon(notificationBuilder);
|
setLargeIcon(notificationBuilder);
|
||||||
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
if (notificationManager.areNotificationsEnabled()) {
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notificationBuilder.build());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
package org.schabi.newpipe.settings.export;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.io.InputStream;
|
||||||
|
import java.io.ObjectInputStream;
|
||||||
|
import java.io.ObjectStreamClass;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An {@link ObjectInputStream} that only allows preferences-related types to be deserialized, to
|
||||||
|
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
|
||||||
|
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
|
||||||
|
* <a href="https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution">
|
||||||
|
* cmu.edu
|
||||||
|
* </a>,
|
||||||
|
* <a href="https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream">
|
||||||
|
* OWASP cheatsheet
|
||||||
|
* </a>,
|
||||||
|
* <a href="https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118">
|
||||||
|
* Apache's {@code ValidatingObjectInputStream}
|
||||||
|
* </a>
|
||||||
|
*/
|
||||||
|
public class PreferencesObjectInputStream extends ObjectInputStream {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Primitive types, strings and other built-in types do not pass through resolveClass() but
|
||||||
|
* instead have a custom encoding; see
|
||||||
|
* <a href="https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152">
|
||||||
|
* official docs</a>.
|
||||||
|
*/
|
||||||
|
private static final Set<String> CLASS_WHITELIST = Set.of(
|
||||||
|
"java.lang.Boolean",
|
||||||
|
"java.lang.Byte",
|
||||||
|
"java.lang.Character",
|
||||||
|
"java.lang.Short",
|
||||||
|
"java.lang.Integer",
|
||||||
|
"java.lang.Long",
|
||||||
|
"java.lang.Float",
|
||||||
|
"java.lang.Double",
|
||||||
|
"java.lang.Void",
|
||||||
|
"java.util.HashMap",
|
||||||
|
"java.util.HashSet"
|
||||||
|
);
|
||||||
|
|
||||||
|
public PreferencesObjectInputStream(final InputStream in) throws IOException {
|
||||||
|
super(in);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected Class<?> resolveClass(final ObjectStreamClass desc)
|
||||||
|
throws ClassNotFoundException, IOException {
|
||||||
|
if (CLASS_WHITELIST.contains(desc.getName())) {
|
||||||
|
return super.resolveClass(desc);
|
||||||
|
} else {
|
||||||
|
throw new ClassNotFoundException("Class not allowed: " + desc.getName());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2024-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.settings.export
|
|
||||||
|
|
||||||
import java.io.IOException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.io.ObjectInputStream
|
|
||||||
import java.io.ObjectStreamClass
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An [ObjectInputStream] that only allows preferences-related types to be deserialized, to
|
|
||||||
* prevent injections. The only allowed types are: all primitive types, all boxed primitive types,
|
|
||||||
* null, strings. HashMap, HashSet and arrays of previously defined types are also allowed. Sources:
|
|
||||||
* [cmu.edu](https://wiki.sei.cmu.edu/confluence/display/java/SER00-J.+Enable+serialization+compatibility+during+class+evolution) * ,
|
|
||||||
* [OWASP cheatsheet](https://cheatsheetseries.owasp.org/cheatsheets/Deserialization_Cheat_Sheet.html#harden-your-own-javaioobjectinputstream) * ,
|
|
||||||
* [Apache's `ValidatingObjectInputStream`](https://commons.apache.org/proper/commons-io/apidocs/src-html/org/apache/commons/io/serialization/ValidatingObjectInputStream.html#line-118) *
|
|
||||||
*/
|
|
||||||
class PreferencesObjectInputStream(stream: InputStream) : ObjectInputStream(stream) {
|
|
||||||
@Throws(ClassNotFoundException::class, IOException::class)
|
|
||||||
override fun resolveClass(desc: ObjectStreamClass): Class<*> {
|
|
||||||
if (desc.name in CLASS_WHITELIST) {
|
|
||||||
return super.resolveClass(desc)
|
|
||||||
} else {
|
|
||||||
throw ClassNotFoundException("Class not allowed: $desc.name")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
/**
|
|
||||||
* Primitive types, strings and other built-in types do not pass through resolveClass() but
|
|
||||||
* instead have a custom encoding; see
|
|
||||||
* [
|
|
||||||
* official docs](https://docs.oracle.com/javase/6/docs/platform/serialization/spec/protocol.html#10152).
|
|
||||||
*/
|
|
||||||
private val CLASS_WHITELIST = setOf<String>(
|
|
||||||
"java.lang.Boolean",
|
|
||||||
"java.lang.Byte",
|
|
||||||
"java.lang.Character",
|
|
||||||
"java.lang.Short",
|
|
||||||
"java.lang.Integer",
|
|
||||||
"java.lang.Long",
|
|
||||||
"java.lang.Float",
|
|
||||||
"java.lang.Double",
|
|
||||||
"java.lang.Void",
|
|
||||||
"java.util.HashMap",
|
|
||||||
"java.util.HashSet"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -26,13 +26,14 @@ data class PreferenceSearchItem(
|
|||||||
val breadcrumbs: String,
|
val breadcrumbs: String,
|
||||||
@XmlRes val searchIndexItemResId: Int
|
@XmlRes val searchIndexItemResId: Int
|
||||||
) {
|
) {
|
||||||
val allRelevantSearchFields: List<String>
|
|
||||||
get() = listOf(title, summary, entries, breadcrumbs)
|
|
||||||
|
|
||||||
fun hasData(): Boolean {
|
fun hasData(): Boolean {
|
||||||
return !key.isEmpty() && !title.isEmpty()
|
return !key.isEmpty() && !title.isEmpty()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getAllRelevantSearchFields(): MutableList<String?> {
|
||||||
|
return mutableListOf(title, summary, entries, breadcrumbs)
|
||||||
|
}
|
||||||
|
|
||||||
override fun toString(): String {
|
override fun toString(): String {
|
||||||
return "PreferenceItem: $title $summary $key"
|
return "PreferenceItem: $title $summary $key"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For preferences with dependencies and multiple use case,
|
||||||
|
* this class can be used to reduce the lines of code.
|
||||||
|
*/
|
||||||
|
public final class DependentPreferenceHelper {
|
||||||
|
|
||||||
|
private DependentPreferenceHelper() {
|
||||||
|
// no instance
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
|
||||||
|
* `Resume playback` and its dependencies are all enabled.
|
||||||
|
*
|
||||||
|
* @param context the Android context
|
||||||
|
* @return returns true if `Resume playback` and `Watch history` are both enabled
|
||||||
|
*/
|
||||||
|
public static boolean getResumePlaybackEnabled(final Context context) {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
return prefs.getBoolean(context.getString(
|
||||||
|
R.string.enable_watch_history_key), true)
|
||||||
|
&& prefs.getBoolean(context.getString(
|
||||||
|
R.string.enable_playback_resume_key), true);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
|
||||||
|
* `Position in lists` and its dependencies are all enabled.
|
||||||
|
*
|
||||||
|
* @param context the Android context
|
||||||
|
* @return returns true if `Positions in lists` and `Watch history` are both enabled
|
||||||
|
*/
|
||||||
|
public static boolean getPositionsInListsEnabled(final Context context) {
|
||||||
|
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
|
|
||||||
|
return prefs.getBoolean(context.getString(
|
||||||
|
R.string.enable_watch_history_key), true)
|
||||||
|
&& prefs.getBoolean(context.getString(
|
||||||
|
R.string.enable_playback_state_lists_key), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For preferences with dependencies and multiple use case,
|
|
||||||
* this class can be used to reduce the lines of code.
|
|
||||||
*/
|
|
||||||
object DependentPreferenceHelper {
|
|
||||||
/**
|
|
||||||
* Option `Resume playback` depends on `Watch history`, this method can be used to retrieve if
|
|
||||||
* `Resume playback` and its dependencies are all enabled.
|
|
||||||
*
|
|
||||||
* @param context the Android context
|
|
||||||
* @return returns true if `Resume playback` and `Watch history` are both enabled
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun getResumePlaybackEnabled(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
|
|
||||||
prefs.getBoolean(context.getString(R.string.enable_playback_resume_key), true)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Option `Position in lists` depends on `Watch history`, this method can be used to retrieve if
|
|
||||||
* `Position in lists` and its dependencies are all enabled.
|
|
||||||
*
|
|
||||||
* @param context the Android context
|
|
||||||
* @return returns true if `Positions in lists` and `Watch history` are both enabled
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun getPositionsInListsEnabled(context: Context): Boolean {
|
|
||||||
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
|
|
||||||
return prefs.getBoolean(context.getString(R.string.enable_watch_history_key), true) &&
|
|
||||||
prefs.getBoolean(context.getString(R.string.enable_playback_state_lists_key), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -48,7 +48,7 @@ public final class KeyboardUtil {
|
|||||||
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
final InputMethodManager imm = ContextCompat.getSystemService(activity,
|
||||||
InputMethodManager.class);
|
InputMethodManager.class);
|
||||||
imm.hideSoftInputFromWindow(editText.getWindowToken(),
|
imm.hideSoftInputFromWindow(editText.getWindowToken(),
|
||||||
InputMethodManager.RESULT_UNCHANGED_SHOWN);
|
InputMethodManager.HIDE_NOT_ALWAYS);
|
||||||
|
|
||||||
editText.clearFocus();
|
editText.clearFocus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.text.Selection;
|
||||||
|
import android.text.Spannable;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.util.external_communication.ShareUtils;
|
||||||
|
import org.schabi.newpipe.views.NewPipeEditText;
|
||||||
|
import org.schabi.newpipe.views.NewPipeTextView;
|
||||||
|
|
||||||
|
public final class NewPipeTextViewHelper {
|
||||||
|
private NewPipeTextViewHelper() {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Share the selected text of {@link NewPipeTextView NewPipeTextViews} and
|
||||||
|
* {@link NewPipeEditText NewPipeEditTexts} with
|
||||||
|
* {@link ShareUtils#shareText(Context, String, String)}.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
|
||||||
|
* using the {@code Share} command of the popup menu which appears when selecting text.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param textView the {@link TextView} on which sharing the selected text. It should be a
|
||||||
|
* {@link NewPipeTextView} or a {@link NewPipeEditText} (even if
|
||||||
|
* {@link TextView standard TextViews} are supported).
|
||||||
|
*/
|
||||||
|
public static void shareSelectedTextWithShareUtils(@NonNull final TextView textView) {
|
||||||
|
final CharSequence textViewText = textView.getText();
|
||||||
|
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText));
|
||||||
|
if (textViewText instanceof Spannable) {
|
||||||
|
Selection.setSelection((Spannable) textViewText, textView.getSelectionEnd());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
private static CharSequence getSelectedText(@NonNull final TextView textView,
|
||||||
|
@Nullable final CharSequence text) {
|
||||||
|
if (!textView.hasSelection() || text == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
final int start = textView.getSelectionStart();
|
||||||
|
final int end = textView.getSelectionEnd();
|
||||||
|
return String.valueOf(start > end ? text.subSequence(end, start)
|
||||||
|
: text.subSequence(start, end));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void shareSelectedTextIfNotNullAndNotEmpty(
|
||||||
|
@NonNull final TextView textView,
|
||||||
|
@Nullable final CharSequence selectedText) {
|
||||||
|
if (selectedText != null && selectedText.length() != 0) {
|
||||||
|
ShareUtils.shareText(textView.getContext(), "", selectedText.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.util
|
|
||||||
|
|
||||||
import android.text.Selection
|
|
||||||
import android.text.Spannable
|
|
||||||
import android.widget.TextView
|
|
||||||
import org.schabi.newpipe.util.external_communication.ShareUtils
|
|
||||||
|
|
||||||
object NewPipeTextViewHelper {
|
|
||||||
/**
|
|
||||||
* Share the selected text of [NewPipeTextViews][org.schabi.newpipe.views.NewPipeTextView] and
|
|
||||||
* [NewPipeEditTexts][org.schabi.newpipe.views.NewPipeEditText] with
|
|
||||||
* [ShareUtils.shareText].
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* This allows EMUI users to get the Android share sheet instead of the EMUI share sheet when
|
|
||||||
* using the `Share` command of the popup menu which appears when selecting text.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param textView the [TextView] on which sharing the selected text. It should be a
|
|
||||||
* [org.schabi.newpipe.views.NewPipeTextView] or a [org.schabi.newpipe.views.NewPipeEditText]
|
|
||||||
* (even if [standard TextViews][TextView] are supported).
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun shareSelectedTextWithShareUtils(textView: TextView) {
|
|
||||||
val textViewText = textView.getText()
|
|
||||||
shareSelectedTextIfNotNullAndNotEmpty(textView, getSelectedText(textView, textViewText))
|
|
||||||
if (textViewText is Spannable) {
|
|
||||||
Selection.setSelection(textViewText, textView.selectionEnd)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getSelectedText(textView: TextView, text: CharSequence?): CharSequence? {
|
|
||||||
if (!textView.hasSelection() || text == null) {
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
val start = textView.selectionStart
|
|
||||||
val end = textView.selectionEnd
|
|
||||||
return if (start > end) {
|
|
||||||
text.subSequence(end, start)
|
|
||||||
} else {
|
|
||||||
text.subSequence(start, end)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun shareSelectedTextIfNotNullAndNotEmpty(
|
|
||||||
textView: TextView,
|
|
||||||
selectedText: CharSequence?
|
|
||||||
) {
|
|
||||||
if (!selectedText.isNullOrEmpty()) {
|
|
||||||
ShareUtils.shareText(textView.context, "", selectedText.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonArray;
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
import com.grack.nanojson.JsonStringWriter;
|
||||||
|
import com.grack.nanojson.JsonWriter;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
public final class PeertubeHelper {
|
||||||
|
private PeertubeHelper() { }
|
||||||
|
|
||||||
|
public static List<PeertubeInstance> getInstanceList(final Context context) {
|
||||||
|
final SharedPreferences sharedPreferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
final String savedInstanceListKey = context.getString(R.string.peertube_instance_list_key);
|
||||||
|
final String savedJson = sharedPreferences.getString(savedInstanceListKey, null);
|
||||||
|
if (null == savedJson) {
|
||||||
|
return List.of(getCurrentInstance());
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
final JsonArray array = JsonParser.object().from(savedJson).getArray("instances");
|
||||||
|
final List<PeertubeInstance> result = new ArrayList<>();
|
||||||
|
for (final Object o : array) {
|
||||||
|
if (o instanceof JsonObject) {
|
||||||
|
final JsonObject instance = (JsonObject) o;
|
||||||
|
final String name = instance.getString("name");
|
||||||
|
final String url = instance.getString("url");
|
||||||
|
result.add(new PeertubeInstance(url, name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
return List.of(getCurrentInstance());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PeertubeInstance selectInstance(final PeertubeInstance instance,
|
||||||
|
final Context context) {
|
||||||
|
final SharedPreferences sharedPreferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
final String selectedInstanceKey =
|
||||||
|
context.getString(R.string.peertube_selected_instance_key);
|
||||||
|
final JsonStringWriter jsonWriter = JsonWriter.string().object();
|
||||||
|
jsonWriter.value("name", instance.getName());
|
||||||
|
jsonWriter.value("url", instance.getUrl());
|
||||||
|
final String jsonToSave = jsonWriter.end().done();
|
||||||
|
sharedPreferences.edit().putString(selectedInstanceKey, jsonToSave).apply();
|
||||||
|
ServiceList.PeerTube.setInstance(instance);
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static PeertubeInstance getCurrentInstance() {
|
||||||
|
return ServiceList.PeerTube.getInstance();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2019-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.grack.nanojson.JsonObject
|
|
||||||
import com.grack.nanojson.JsonParser
|
|
||||||
import com.grack.nanojson.JsonWriter
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
|
|
||||||
|
|
||||||
object PeertubeHelper {
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
val currentInstance: PeertubeInstance
|
|
||||||
get() = ServiceList.PeerTube.instance
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getInstanceList(context: Context): List<PeertubeInstance> {
|
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val savedInstanceListKey = context.getString(R.string.peertube_instance_list_key)
|
|
||||||
val savedJson = sharedPreferences.getString(savedInstanceListKey, null)
|
|
||||||
?: return listOf(currentInstance)
|
|
||||||
|
|
||||||
return runCatching {
|
|
||||||
JsonParser.`object`().from(savedJson).getArray("instances")
|
|
||||||
.filterIsInstance<JsonObject>()
|
|
||||||
.map { PeertubeInstance(it.getString("url"), it.getString("name")) }
|
|
||||||
}.getOrDefault(listOf(currentInstance))
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun selectInstance(instance: PeertubeInstance, context: Context): PeertubeInstance {
|
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val selectedInstanceKey = context.getString(R.string.peertube_selected_instance_key)
|
|
||||||
|
|
||||||
val jsonWriter = JsonWriter.string().`object`()
|
|
||||||
jsonWriter.value("name", instance.name)
|
|
||||||
jsonWriter.value("url", instance.url)
|
|
||||||
val jsonToSave = jsonWriter.end().done()
|
|
||||||
|
|
||||||
sharedPreferences.edit { putString(selectedInstanceKey, jsonToSave) }
|
|
||||||
ServiceList.PeerTube.instance = instance
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.databinding.PlaylistControlBinding;
|
||||||
|
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder;
|
||||||
|
import org.schabi.newpipe.player.PlayerType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for play buttons and their respective click listeners.
|
||||||
|
*/
|
||||||
|
public final class PlayButtonHelper {
|
||||||
|
|
||||||
|
private PlayButtonHelper() {
|
||||||
|
// utility class
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize {@link android.view.View.OnClickListener OnClickListener}
|
||||||
|
* and {@link android.view.View.OnLongClickListener OnLongClickListener} for playlist control
|
||||||
|
* buttons defined in {@link R.layout#playlist_control}.
|
||||||
|
*
|
||||||
|
* @param activity The activity to use for the {@link android.widget.Toast Toast}.
|
||||||
|
* @param playlistControlBinding The binding of the
|
||||||
|
* {@link R.layout#playlist_control playlist control layout}.
|
||||||
|
* @param fragment The fragment to get the play queue from.
|
||||||
|
*/
|
||||||
|
public static void initPlaylistControlClickListener(
|
||||||
|
@NonNull final AppCompatActivity activity,
|
||||||
|
@NonNull final PlaylistControlBinding playlistControlBinding,
|
||||||
|
@NonNull final PlaylistControlViewHolder fragment) {
|
||||||
|
// click listener
|
||||||
|
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener(view -> {
|
||||||
|
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue());
|
||||||
|
showHoldToAppendToastIfNeeded(activity);
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener(view -> {
|
||||||
|
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false);
|
||||||
|
showHoldToAppendToastIfNeeded(activity);
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener(view -> {
|
||||||
|
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false);
|
||||||
|
showHoldToAppendToastIfNeeded(activity);
|
||||||
|
});
|
||||||
|
|
||||||
|
// long click listener
|
||||||
|
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener(view -> {
|
||||||
|
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO);
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the "hold to append" toast if the corresponding preference is enabled.
|
||||||
|
*
|
||||||
|
* @param context The context to show the toast.
|
||||||
|
*/
|
||||||
|
private static void showHoldToAppendToastIfNeeded(@NonNull final Context context) {
|
||||||
|
if (shouldShowHoldToAppendTip(context)) {
|
||||||
|
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the "hold to append" toast should be shown.
|
||||||
|
*
|
||||||
|
* <p>
|
||||||
|
* The tip is shown if the corresponding preference is enabled.
|
||||||
|
* This is the default behaviour.
|
||||||
|
* </p>
|
||||||
|
*
|
||||||
|
* @param context The context to get the preference.
|
||||||
|
* @return {@code true} if the tip should be shown, {@code false} otherwise.
|
||||||
|
*/
|
||||||
|
public static boolean shouldShowHoldToAppendTip(@NonNull final Context context) {
|
||||||
|
return PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getBoolean(context.getString(R.string.show_hold_to_append_key), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,96 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2023-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.View
|
|
||||||
import android.view.View.OnLongClickListener
|
|
||||||
import android.widget.Toast
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.databinding.PlaylistControlBinding
|
|
||||||
import org.schabi.newpipe.fragments.list.playlist.PlaylistControlViewHolder
|
|
||||||
import org.schabi.newpipe.player.PlayerType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for play buttons and their respective click listeners.
|
|
||||||
*/
|
|
||||||
object PlayButtonHelper {
|
|
||||||
/**
|
|
||||||
* Initialize [OnClickListener][View.OnClickListener]
|
|
||||||
* and [OnLongClickListener][OnLongClickListener] for playlist control
|
|
||||||
* buttons defined in [R.layout.playlist_control].
|
|
||||||
*
|
|
||||||
* @param activity The activity to use for the [Toast][Toast].
|
|
||||||
* @param playlistControlBinding The binding of the
|
|
||||||
* [playlist control layout][R.layout.playlist_control].
|
|
||||||
* @param fragment The fragment to get the play queue from.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun initPlaylistControlClickListener(
|
|
||||||
activity: AppCompatActivity,
|
|
||||||
playlistControlBinding: PlaylistControlBinding,
|
|
||||||
fragment: PlaylistControlViewHolder
|
|
||||||
) {
|
|
||||||
// click listener
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnClickListener {
|
|
||||||
NavigationHelper.playOnMainPlayer(activity, fragment.getPlayQueue())
|
|
||||||
showHoldToAppendToastIfNeeded(activity)
|
|
||||||
}
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnClickListener {
|
|
||||||
NavigationHelper.playOnPopupPlayer(activity, fragment.getPlayQueue(), false)
|
|
||||||
showHoldToAppendToastIfNeeded(activity)
|
|
||||||
}
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnClickListener {
|
|
||||||
NavigationHelper.playOnBackgroundPlayer(activity, fragment.getPlayQueue(), false)
|
|
||||||
showHoldToAppendToastIfNeeded(activity)
|
|
||||||
}
|
|
||||||
|
|
||||||
// long click listener
|
|
||||||
playlistControlBinding.playlistCtrlPlayAllButton.setOnLongClickListener {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.MAIN)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
playlistControlBinding.playlistCtrlPlayPopupButton.setOnLongClickListener {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.POPUP)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
playlistControlBinding.playlistCtrlPlayBgButton.setOnLongClickListener {
|
|
||||||
NavigationHelper.enqueueOnPlayer(activity, fragment.getPlayQueue(), PlayerType.AUDIO)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Show the "hold to append" toast if the corresponding preference is enabled.
|
|
||||||
*
|
|
||||||
* @param context The context to show the toast.
|
|
||||||
*/
|
|
||||||
private fun showHoldToAppendToastIfNeeded(context: Context) {
|
|
||||||
if (shouldShowHoldToAppendTip(context)) {
|
|
||||||
Toast.makeText(context, R.string.hold_to_append, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the "hold to append" toast should be shown.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* The tip is shown if the corresponding preference is enabled.
|
|
||||||
* This is the default behaviour.
|
|
||||||
*
|
|
||||||
*
|
|
||||||
* @param context The context to get the preference.
|
|
||||||
* @return `true` if the tip should be shown, `false` otherwise.
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun shouldShowHoldToAppendTip(context: Context): Boolean {
|
|
||||||
return PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getBoolean(context.getString(R.string.show_hold_to_append_key), true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
213
app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
Normal file
213
app/src/main/java/org/schabi/newpipe/util/ServiceHelper.java
Normal file
@@ -0,0 +1,213 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import static org.schabi.newpipe.extractor.ServiceList.SoundCloud;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.DrawableRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.preference.PreferenceManager;
|
||||||
|
|
||||||
|
import com.grack.nanojson.JsonObject;
|
||||||
|
import com.grack.nanojson.JsonParser;
|
||||||
|
import com.grack.nanojson.JsonParserException;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.NewPipe;
|
||||||
|
import org.schabi.newpipe.extractor.ServiceList;
|
||||||
|
import org.schabi.newpipe.extractor.StreamingService;
|
||||||
|
import org.schabi.newpipe.extractor.exceptions.ExtractionException;
|
||||||
|
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance;
|
||||||
|
|
||||||
|
import java.util.Optional;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
public final class ServiceHelper {
|
||||||
|
private static final StreamingService DEFAULT_FALLBACK_SERVICE = ServiceList.YouTube;
|
||||||
|
|
||||||
|
private ServiceHelper() { }
|
||||||
|
|
||||||
|
@DrawableRes
|
||||||
|
public static int getIcon(final int serviceId) {
|
||||||
|
switch (serviceId) {
|
||||||
|
case 0:
|
||||||
|
return R.drawable.ic_smart_display;
|
||||||
|
case 1:
|
||||||
|
return R.drawable.ic_cloud;
|
||||||
|
case 2:
|
||||||
|
return R.drawable.ic_placeholder_media_ccc;
|
||||||
|
case 3:
|
||||||
|
return R.drawable.ic_placeholder_peertube;
|
||||||
|
case 4:
|
||||||
|
return R.drawable.ic_placeholder_bandcamp;
|
||||||
|
default:
|
||||||
|
return R.drawable.ic_circle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getTranslatedFilterString(final String filter, final Context c) {
|
||||||
|
switch (filter) {
|
||||||
|
case "all":
|
||||||
|
return c.getString(R.string.all);
|
||||||
|
case "videos":
|
||||||
|
case "sepia_videos":
|
||||||
|
case "music_videos":
|
||||||
|
return c.getString(R.string.videos_string);
|
||||||
|
case "channels":
|
||||||
|
return c.getString(R.string.channels);
|
||||||
|
case "playlists":
|
||||||
|
case "music_playlists":
|
||||||
|
return c.getString(R.string.playlists);
|
||||||
|
case "tracks":
|
||||||
|
return c.getString(R.string.tracks);
|
||||||
|
case "users":
|
||||||
|
return c.getString(R.string.users);
|
||||||
|
case "conferences":
|
||||||
|
return c.getString(R.string.conferences);
|
||||||
|
case "events":
|
||||||
|
return c.getString(R.string.events);
|
||||||
|
case "music_songs":
|
||||||
|
return c.getString(R.string.songs);
|
||||||
|
case "music_albums":
|
||||||
|
return c.getString(R.string.albums);
|
||||||
|
case "music_artists":
|
||||||
|
return c.getString(R.string.artists);
|
||||||
|
default:
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a resource string with instructions for importing subscriptions for each service.
|
||||||
|
*
|
||||||
|
* @param serviceId service to get the instructions for
|
||||||
|
* @return the string resource containing the instructions or -1 if the service don't support it
|
||||||
|
*/
|
||||||
|
@StringRes
|
||||||
|
public static int getImportInstructions(final int serviceId) {
|
||||||
|
switch (serviceId) {
|
||||||
|
case 0:
|
||||||
|
return R.string.import_youtube_instructions;
|
||||||
|
case 1:
|
||||||
|
return R.string.import_soundcloud_instructions;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For services that support importing from a channel url, return a hint that will
|
||||||
|
* be used in the EditText that the user will type in his channel url.
|
||||||
|
*
|
||||||
|
* @param serviceId service to get the hint for
|
||||||
|
* @return the hint's string resource or -1 if the service don't support it
|
||||||
|
*/
|
||||||
|
@StringRes
|
||||||
|
public static int getImportInstructionsHint(final int serviceId) {
|
||||||
|
switch (serviceId) {
|
||||||
|
case 1:
|
||||||
|
return R.string.import_soundcloud_instructions_hint;
|
||||||
|
default:
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int getSelectedServiceId(final Context context) {
|
||||||
|
return Optional.ofNullable(getSelectedService(context))
|
||||||
|
.orElse(DEFAULT_FALLBACK_SERVICE)
|
||||||
|
.getServiceId();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static StreamingService getSelectedService(final Context context) {
|
||||||
|
final String serviceName = PreferenceManager.getDefaultSharedPreferences(context)
|
||||||
|
.getString(context.getString(R.string.current_service_key),
|
||||||
|
context.getString(R.string.default_service_value));
|
||||||
|
|
||||||
|
try {
|
||||||
|
return NewPipe.getService(serviceName);
|
||||||
|
} catch (final ExtractionException e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
public static String getNameOfServiceById(final int serviceId) {
|
||||||
|
return ServiceList.all().stream()
|
||||||
|
.filter(s -> s.getServiceId() == serviceId)
|
||||||
|
.findFirst()
|
||||||
|
.map(StreamingService::getServiceInfo)
|
||||||
|
.map(StreamingService.ServiceInfo::getName)
|
||||||
|
.orElse("<unknown>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param serviceId the id of the service
|
||||||
|
* @return the service corresponding to the provided id
|
||||||
|
* @throws java.util.NoSuchElementException if there is no service with the provided id
|
||||||
|
*/
|
||||||
|
@NonNull
|
||||||
|
public static StreamingService getServiceById(final int serviceId) {
|
||||||
|
return ServiceList.all().stream()
|
||||||
|
.filter(s -> s.getServiceId() == serviceId)
|
||||||
|
.findFirst()
|
||||||
|
.orElseThrow();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void setSelectedServiceId(final Context context, final int serviceId) {
|
||||||
|
String serviceName;
|
||||||
|
try {
|
||||||
|
serviceName = NewPipe.getService(serviceId).getServiceInfo().getName();
|
||||||
|
} catch (final ExtractionException e) {
|
||||||
|
serviceName = DEFAULT_FALLBACK_SERVICE.getServiceInfo().getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedServicePreferences(context, serviceName);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void setSelectedServicePreferences(final Context context,
|
||||||
|
final String serviceName) {
|
||||||
|
PreferenceManager.getDefaultSharedPreferences(context).edit().
|
||||||
|
putString(context.getString(R.string.current_service_key), serviceName).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long getCacheExpirationMillis(final int serviceId) {
|
||||||
|
if (serviceId == SoundCloud.getServiceId()) {
|
||||||
|
return TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
|
||||||
|
} else {
|
||||||
|
return TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initService(final Context context, final int serviceId) {
|
||||||
|
if (serviceId == ServiceList.PeerTube.getServiceId()) {
|
||||||
|
final SharedPreferences sharedPreferences = PreferenceManager
|
||||||
|
.getDefaultSharedPreferences(context);
|
||||||
|
final String json = sharedPreferences.getString(context.getString(
|
||||||
|
R.string.peertube_selected_instance_key), null);
|
||||||
|
if (null == json) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final JsonObject jsonObject;
|
||||||
|
try {
|
||||||
|
jsonObject = JsonParser.object().from(json);
|
||||||
|
} catch (final JsonParserException e) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final String name = jsonObject.getString("name");
|
||||||
|
final String url = jsonObject.getString("url");
|
||||||
|
final PeertubeInstance instance = new PeertubeInstance(url, name);
|
||||||
|
ServiceList.PeerTube.setInstance(instance);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void initServices(final Context context) {
|
||||||
|
for (final StreamingService s : ServiceList.all()) {
|
||||||
|
initService(context, s.getServiceId());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2018-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.util
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import androidx.annotation.DrawableRes
|
|
||||||
import androidx.annotation.StringRes
|
|
||||||
import androidx.core.content.edit
|
|
||||||
import androidx.preference.PreferenceManager
|
|
||||||
import com.grack.nanojson.JsonParser
|
|
||||||
import java.util.concurrent.TimeUnit
|
|
||||||
import org.schabi.newpipe.R
|
|
||||||
import org.schabi.newpipe.extractor.NewPipe
|
|
||||||
import org.schabi.newpipe.extractor.ServiceList
|
|
||||||
import org.schabi.newpipe.extractor.StreamingService
|
|
||||||
import org.schabi.newpipe.extractor.services.peertube.PeertubeInstance
|
|
||||||
import org.schabi.newpipe.ktx.getStringSafe
|
|
||||||
|
|
||||||
object ServiceHelper {
|
|
||||||
private val DEFAULT_FALLBACK_SERVICE: StreamingService = ServiceList.YouTube
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
@DrawableRes
|
|
||||||
fun getIcon(serviceId: Int): Int {
|
|
||||||
return when (serviceId) {
|
|
||||||
0 -> R.drawable.ic_smart_display
|
|
||||||
1 -> R.drawable.ic_cloud
|
|
||||||
2 -> R.drawable.ic_placeholder_media_ccc
|
|
||||||
3 -> R.drawable.ic_placeholder_peertube
|
|
||||||
4 -> R.drawable.ic_placeholder_bandcamp
|
|
||||||
else -> R.drawable.ic_circle
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getTranslatedFilterString(filter: String, context: Context): String {
|
|
||||||
return when (filter) {
|
|
||||||
"all" -> context.getString(R.string.all)
|
|
||||||
"videos", "sepia_videos", "music_videos" -> context.getString(R.string.videos_string)
|
|
||||||
"channels" -> context.getString(R.string.channels)
|
|
||||||
"playlists", "music_playlists" -> context.getString(R.string.playlists)
|
|
||||||
"tracks" -> context.getString(R.string.tracks)
|
|
||||||
"users" -> context.getString(R.string.users)
|
|
||||||
"conferences" -> context.getString(R.string.conferences)
|
|
||||||
"events" -> context.getString(R.string.events)
|
|
||||||
"music_songs" -> context.getString(R.string.songs)
|
|
||||||
"music_albums" -> context.getString(R.string.albums)
|
|
||||||
"music_artists" -> context.getString(R.string.artists)
|
|
||||||
else -> filter
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get a resource string with instructions for importing subscriptions for each service.
|
|
||||||
*
|
|
||||||
* @param serviceId service to get the instructions for
|
|
||||||
* @return the string resource containing the instructions or -1 if the service don't support it
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
@StringRes
|
|
||||||
fun getImportInstructions(serviceId: Int): Int {
|
|
||||||
return when (serviceId) {
|
|
||||||
0 -> R.string.import_youtube_instructions
|
|
||||||
1 -> R.string.import_soundcloud_instructions
|
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* For services that support importing from a channel url, return a hint that will
|
|
||||||
* be used in the EditText that the user will type in his channel url.
|
|
||||||
*
|
|
||||||
* @param serviceId service to get the hint for
|
|
||||||
* @return the hint's string resource or -1 if the service don't support it
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
@StringRes
|
|
||||||
fun getImportInstructionsHint(serviceId: Int): Int {
|
|
||||||
return when (serviceId) {
|
|
||||||
1 -> R.string.import_soundcloud_instructions_hint
|
|
||||||
else -> -1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getSelectedServiceId(context: Context): Int {
|
|
||||||
return (getSelectedService(context) ?: DEFAULT_FALLBACK_SERVICE).serviceId
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getSelectedService(context: Context): StreamingService? {
|
|
||||||
val serviceName: String = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
.getStringSafe(
|
|
||||||
context.getString(R.string.current_service_key),
|
|
||||||
context.getString(R.string.default_service_value)
|
|
||||||
)
|
|
||||||
|
|
||||||
return runCatching { NewPipe.getService(serviceName) }.getOrNull()
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getNameOfServiceById(serviceId: Int): String {
|
|
||||||
return ServiceList.all().stream()
|
|
||||||
.filter { it.serviceId == serviceId }
|
|
||||||
.findFirst()
|
|
||||||
.map(StreamingService::getServiceInfo)
|
|
||||||
.map(StreamingService.ServiceInfo::getName)
|
|
||||||
.orElse("<unknown>")
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @param serviceId the id of the service
|
|
||||||
* @return the service corresponding to the provided id
|
|
||||||
* @throws java.util.NoSuchElementException if there is no service with the provided id
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun getServiceById(serviceId: Int): StreamingService {
|
|
||||||
return ServiceList.all().firstNotNullOf { it.takeIf { it.serviceId == serviceId } }
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun setSelectedServiceId(context: Context, serviceId: Int) {
|
|
||||||
val serviceName = runCatching { NewPipe.getService(serviceId).serviceInfo.name }
|
|
||||||
.getOrDefault(DEFAULT_FALLBACK_SERVICE.serviceInfo.name)
|
|
||||||
|
|
||||||
setSelectedServicePreferences(context, serviceName)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setSelectedServicePreferences(context: Context, serviceName: String?) {
|
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
sharedPreferences.edit { putString(context.getString(R.string.current_service_key), serviceName) }
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun getCacheExpirationMillis(serviceId: Int): Long {
|
|
||||||
return if (serviceId == ServiceList.SoundCloud.serviceId) {
|
|
||||||
TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES)
|
|
||||||
} else {
|
|
||||||
TimeUnit.MILLISECONDS.convert(1, TimeUnit.HOURS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initService(context: Context, serviceId: Int) {
|
|
||||||
if (serviceId == ServiceList.PeerTube.serviceId) {
|
|
||||||
val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
|
|
||||||
val json = sharedPreferences.getString(
|
|
||||||
context.getString(R.string.peertube_selected_instance_key),
|
|
||||||
null
|
|
||||||
) ?: return
|
|
||||||
|
|
||||||
val jsonObject = runCatching { JsonParser.`object`().from(json) }
|
|
||||||
.getOrElse { return@initService }
|
|
||||||
|
|
||||||
ServiceList.PeerTube.instance = PeertubeInstance(
|
|
||||||
jsonObject.getString("url"),
|
|
||||||
jsonObject.getString("name")
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun initServices(context: Context) {
|
|
||||||
ServiceList.all().forEach { initService(context, it.serviceId) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility class for {@link StreamType}.
|
||||||
|
*/
|
||||||
|
public final class StreamTypeUtil {
|
||||||
|
private StreamTypeUtil() {
|
||||||
|
// No impl pls
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the {@link StreamType} of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is {@link StreamType#AUDIO_STREAM},
|
||||||
|
* {@link StreamType#AUDIO_LIVE_STREAM} or {@link StreamType#POST_LIVE_AUDIO_STREAM}
|
||||||
|
*/
|
||||||
|
public static boolean isAudio(final StreamType streamType) {
|
||||||
|
return streamType == StreamType.AUDIO_STREAM
|
||||||
|
|| streamType == StreamType.AUDIO_LIVE_STREAM
|
||||||
|
|| streamType == StreamType.POST_LIVE_AUDIO_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the {@link StreamType} of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is {@link StreamType#VIDEO_STREAM},
|
||||||
|
* {@link StreamType#LIVE_STREAM} or {@link StreamType#POST_LIVE_STREAM}
|
||||||
|
*/
|
||||||
|
public static boolean isVideo(final StreamType streamType) {
|
||||||
|
return streamType == StreamType.VIDEO_STREAM
|
||||||
|
|| streamType == StreamType.LIVE_STREAM
|
||||||
|
|| streamType == StreamType.POST_LIVE_STREAM;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the {@link StreamType} of a stream is a livestream.
|
||||||
|
*
|
||||||
|
* @param streamType the stream type of the stream
|
||||||
|
* @return whether the stream type is {@link StreamType#LIVE_STREAM} or
|
||||||
|
* {@link StreamType#AUDIO_LIVE_STREAM}
|
||||||
|
*/
|
||||||
|
public static boolean isLiveStream(final StreamType streamType) {
|
||||||
|
return streamType == StreamType.LIVE_STREAM
|
||||||
|
|| streamType == StreamType.AUDIO_LIVE_STREAM;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
/*
|
|
||||||
* SPDX-FileCopyrightText: 2021-2026 NewPipe contributors <https://newpipe.net>
|
|
||||||
* SPDX-License-Identifier: GPL-3.0-or-later
|
|
||||||
*/
|
|
||||||
|
|
||||||
package org.schabi.newpipe.util
|
|
||||||
|
|
||||||
import org.schabi.newpipe.extractor.stream.StreamType
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Utility class for [StreamType].
|
|
||||||
*/
|
|
||||||
object StreamTypeUtil {
|
|
||||||
/**
|
|
||||||
* Check if the [StreamType] of a stream is a livestream.
|
|
||||||
*
|
|
||||||
* @param streamType the stream type of the stream
|
|
||||||
* @return whether the stream type is [StreamType.AUDIO_STREAM],
|
|
||||||
* [StreamType.AUDIO_LIVE_STREAM] or [StreamType.POST_LIVE_AUDIO_STREAM]
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun isAudio(streamType: StreamType): Boolean {
|
|
||||||
return streamType == StreamType.AUDIO_STREAM ||
|
|
||||||
streamType == StreamType.AUDIO_LIVE_STREAM ||
|
|
||||||
streamType == StreamType.POST_LIVE_AUDIO_STREAM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the [StreamType] of a stream is a livestream.
|
|
||||||
*
|
|
||||||
* @param streamType the stream type of the stream
|
|
||||||
* @return whether the stream type is [StreamType.VIDEO_STREAM],
|
|
||||||
* [StreamType.LIVE_STREAM] or [StreamType.POST_LIVE_STREAM]
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun isVideo(streamType: StreamType): Boolean {
|
|
||||||
return streamType == StreamType.VIDEO_STREAM ||
|
|
||||||
streamType == StreamType.LIVE_STREAM ||
|
|
||||||
streamType == StreamType.POST_LIVE_STREAM
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the [StreamType] of a stream is a livestream.
|
|
||||||
*
|
|
||||||
* @param streamType the stream type of the stream
|
|
||||||
* @return whether the stream type is [StreamType.LIVE_STREAM] or
|
|
||||||
* [StreamType.AUDIO_LIVE_STREAM]
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun isLiveStream(streamType: StreamType): Boolean {
|
|
||||||
return streamType == StreamType.LIVE_STREAM ||
|
|
||||||
streamType == StreamType.AUDIO_LIVE_STREAM
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,6 @@ import android.view.Window;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.appcompat.view.WindowCallbackWrapper;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
@@ -232,7 +231,7 @@ public final class FocusOverlayView extends Drawable implements
|
|||||||
// Unfortunately many such forms of "scrolling" do not count as scrolling for purpose
|
// Unfortunately many such forms of "scrolling" do not count as scrolling for purpose
|
||||||
// of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly
|
// of dispatching ViewTreeObserver callbacks, so we have to intercept them by directly
|
||||||
// receiving keys from Window.
|
// receiving keys from Window.
|
||||||
window.setCallback(new WindowCallbackWrapper(window.getCallback()) {
|
window.setCallback(new SimpleWindowCallback(window.getCallback()) {
|
||||||
@Override
|
@Override
|
||||||
public boolean dispatchKeyEvent(final KeyEvent event) {
|
public boolean dispatchKeyEvent(final KeyEvent event) {
|
||||||
final boolean res = super.dispatchKeyEvent(event);
|
final boolean res = super.dispatchKeyEvent(event);
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
/*
|
||||||
|
* SPDX-FileCopyrightText: 2026 NewPipe e.V. <https://newpipe-ev.de>
|
||||||
|
* SPDX-License-Identifier: GPL-3.0-or-later
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.schabi.newpipe.views
|
||||||
|
|
||||||
|
import android.os.Build
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.KeyboardShortcutGroup
|
||||||
|
import android.view.Menu
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
|
||||||
|
open class SimpleWindowCallback(private val baseCallback: Window.Callback) :
|
||||||
|
Window.Callback by baseCallback {
|
||||||
|
|
||||||
|
override fun dispatchKeyEvent(event: KeyEvent?): Boolean {
|
||||||
|
return baseCallback.dispatchKeyEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
override fun onPointerCaptureChanged(hasCapture: Boolean) {
|
||||||
|
baseCallback.onPointerCaptureChanged(hasCapture)
|
||||||
|
}
|
||||||
|
|
||||||
|
@RequiresApi(Build.VERSION_CODES.N)
|
||||||
|
override fun onProvideKeyboardShortcuts(
|
||||||
|
data: List<KeyboardShortcutGroup?>?,
|
||||||
|
menu: Menu?,
|
||||||
|
deviceId: Int
|
||||||
|
) {
|
||||||
|
baseCallback.onProvideKeyboardShortcuts(data, menu, deviceId)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,7 +21,7 @@ import static us.shandian.giga.get.DownloadMission.ERROR_HTTP_FORBIDDEN;
|
|||||||
* Single-threaded fallback mode
|
* Single-threaded fallback mode
|
||||||
*/
|
*/
|
||||||
public class DownloadRunnableFallback extends Thread {
|
public class DownloadRunnableFallback extends Thread {
|
||||||
private static final String TAG = "DownloadRunnableFallback";
|
private static final String TAG = DownloadRunnableFallback.class.getSimpleName();
|
||||||
|
|
||||||
private final DownloadMission mMission;
|
private final DownloadMission mMission;
|
||||||
|
|
||||||
|
|||||||
@@ -102,14 +102,23 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||||||
db.beginTransaction();
|
db.beginTransaction();
|
||||||
while (cursor.moveToNext()) {
|
while (cursor.moveToNext()) {
|
||||||
ContentValues values = new ContentValues();
|
ContentValues values = new ContentValues();
|
||||||
values.put(KEY_SOURCE, cursor.getString(cursor.getColumnIndex(KEY_SOURCE)));
|
values.put(
|
||||||
values.put(KEY_DONE, cursor.getString(cursor.getColumnIndex(KEY_DONE)));
|
KEY_SOURCE,
|
||||||
values.put(KEY_TIMESTAMP, cursor.getLong(cursor.getColumnIndex(KEY_TIMESTAMP)));
|
cursor.getString(cursor.getColumnIndexOrThrow(KEY_SOURCE))
|
||||||
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndex(KEY_KIND)));
|
);
|
||||||
|
values.put(
|
||||||
|
KEY_DONE,
|
||||||
|
cursor.getString(cursor.getColumnIndexOrThrow(KEY_DONE))
|
||||||
|
);
|
||||||
|
values.put(
|
||||||
|
KEY_TIMESTAMP,
|
||||||
|
cursor.getLong(cursor.getColumnIndexOrThrow(KEY_TIMESTAMP))
|
||||||
|
);
|
||||||
|
values.put(KEY_KIND, cursor.getString(cursor.getColumnIndexOrThrow(KEY_KIND)));
|
||||||
values.put(KEY_PATH, Uri.fromFile(
|
values.put(KEY_PATH, Uri.fromFile(
|
||||||
new File(
|
new File(
|
||||||
cursor.getString(cursor.getColumnIndex(KEY_LOCATION)),
|
cursor.getString(cursor.getColumnIndexOrThrow(KEY_LOCATION)),
|
||||||
cursor.getString(cursor.getColumnIndex(KEY_NAME))
|
cursor.getString(cursor.getColumnIndexOrThrow(KEY_NAME))
|
||||||
)
|
)
|
||||||
).toString());
|
).toString());
|
||||||
|
|
||||||
@@ -141,7 +150,8 @@ public class FinishedMissionStore extends SQLiteOpenHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private FinishedMission getMissionFromCursor(Cursor cursor) {
|
private FinishedMission getMissionFromCursor(Cursor cursor) {
|
||||||
String kind = Objects.requireNonNull(cursor).getString(cursor.getColumnIndex(KEY_KIND));
|
String kind = Objects.requireNonNull(cursor)
|
||||||
|
.getString(cursor.getColumnIndexOrThrow(KEY_KIND));
|
||||||
if (kind == null || kind.isEmpty()) kind = "?";
|
if (kind == null || kind.isEmpty()) kind = "?";
|
||||||
|
|
||||||
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
|
String path = cursor.getString(cursor.getColumnIndexOrThrow(KEY_PATH));
|
||||||
|
|||||||
@@ -632,103 +632,95 @@ public class MissionAdapter extends Adapter<ViewHolder> implements Handler.Callb
|
|||||||
DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null;
|
DownloadMission mission = h.item.mission instanceof DownloadMission ? (DownloadMission) h.item.mission : null;
|
||||||
|
|
||||||
if (mission != null) {
|
if (mission != null) {
|
||||||
switch (id) {
|
if (id == R.id.start) {
|
||||||
case R.id.start:
|
h.status.setText(UNDEFINED_PROGRESS);
|
||||||
h.status.setText(UNDEFINED_PROGRESS);
|
mDownloadManager.resumeMission(mission);
|
||||||
mDownloadManager.resumeMission(mission);
|
return true;
|
||||||
return true;
|
} else if (id == R.id.pause) {
|
||||||
case R.id.pause:
|
mDownloadManager.pauseMission(mission);
|
||||||
mDownloadManager.pauseMission(mission);
|
return true;
|
||||||
return true;
|
} else if (id == R.id.error_message_view) {
|
||||||
case R.id.error_message_view:
|
showError(mission);
|
||||||
showError(mission);
|
return true;
|
||||||
return true;
|
} else if (id == R.id.queue) {
|
||||||
case R.id.queue:
|
boolean flag = !h.queue.isChecked();
|
||||||
boolean flag = !h.queue.isChecked();
|
h.queue.setChecked(flag);
|
||||||
h.queue.setChecked(flag);
|
mission.setEnqueued(flag);
|
||||||
mission.setEnqueued(flag);
|
updateProgress(h);
|
||||||
updateProgress(h);
|
return true;
|
||||||
return true;
|
} else if (id == R.id.retry) {
|
||||||
case R.id.retry:
|
if (mission.isPsRunning()) {
|
||||||
if (mission.isPsRunning()) {
|
mission.psContinue(true);
|
||||||
mission.psContinue(true);
|
} else {
|
||||||
} else {
|
mDownloadManager.tryRecover(mission);
|
||||||
mDownloadManager.tryRecover(mission);
|
if (mission.storage.isInvalid())
|
||||||
if (mission.storage.isInvalid())
|
mRecover.tryRecover(mission);
|
||||||
mRecover.tryRecover(mission);
|
else
|
||||||
else
|
recoverMission(mission);
|
||||||
recoverMission(mission);
|
}
|
||||||
}
|
return true;
|
||||||
return true;
|
} else if (id == R.id.cancel) {
|
||||||
case R.id.cancel:
|
mission.psContinue(false);
|
||||||
mission.psContinue(false);
|
return false;
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch (id) {
|
if (id == R.id.menu_item_share) {
|
||||||
case R.id.menu_item_share:
|
shareFile(h.item.mission);
|
||||||
shareFile(h.item.mission);
|
return true;
|
||||||
return true;
|
} else if (id == R.id.delete) {// delete the entry and the file
|
||||||
case R.id.delete:
|
mDeleter.append(h.item.mission, true);
|
||||||
// delete the entry and the file
|
applyChanges();
|
||||||
|
checkMasterButtonsVisibility();
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.delete_entry) {// just delete the entry
|
||||||
|
mDeleter.append(h.item.mission, false);
|
||||||
|
applyChanges();
|
||||||
|
checkMasterButtonsVisibility();
|
||||||
|
return true;
|
||||||
|
} else if (id == R.id.md5 || id == R.id.sha1) {
|
||||||
|
final StoredFileHelper storage = h.item.mission.storage;
|
||||||
|
if (!storage.existsAsFile()) {
|
||||||
|
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
|
||||||
mDeleter.append(h.item.mission, true);
|
mDeleter.append(h.item.mission, true);
|
||||||
applyChanges();
|
applyChanges();
|
||||||
checkMasterButtonsVisibility();
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.delete_entry:
|
}
|
||||||
// just delete the entry
|
final NotificationManager notificationManager
|
||||||
mDeleter.append(h.item.mission, false);
|
= ContextCompat.getSystemService(mContext, NotificationManager.class);
|
||||||
applyChanges();
|
final NotificationCompat.Builder progressNotificationBuilder
|
||||||
checkMasterButtonsVisibility();
|
= new NotificationCompat.Builder(mContext,
|
||||||
return true;
|
mContext.getString(R.string.hash_channel_id))
|
||||||
case R.id.md5:
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
case R.id.sha1:
|
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
||||||
final StoredFileHelper storage = h.item.mission.storage;
|
.setContentTitle(mContext.getString(R.string.msg_calculating_hash))
|
||||||
if (!storage.existsAsFile()) {
|
.setContentText(mContext.getString(R.string.msg_wait))
|
||||||
Toast.makeText(mContext, R.string.missing_file, Toast.LENGTH_SHORT).show();
|
.setProgress(0, 0, true)
|
||||||
mDeleter.append(h.item.mission, true);
|
.setOngoing(true);
|
||||||
applyChanges();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
final NotificationManager notificationManager
|
|
||||||
= ContextCompat.getSystemService(mContext, NotificationManager.class);
|
|
||||||
final NotificationCompat.Builder progressNotificationBuilder
|
|
||||||
= new NotificationCompat.Builder(mContext,
|
|
||||||
mContext.getString(R.string.hash_channel_id))
|
|
||||||
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
|
||||||
.setSmallIcon(R.drawable.ic_newpipe_triangle_white)
|
|
||||||
.setContentTitle(mContext.getString(R.string.msg_calculating_hash))
|
|
||||||
.setContentText(mContext.getString(R.string.msg_wait))
|
|
||||||
.setProgress(0, 0, true)
|
|
||||||
.setOngoing(true);
|
|
||||||
|
|
||||||
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
|
notificationManager.notify(HASH_NOTIFICATION_ID, progressNotificationBuilder
|
||||||
.build());
|
.build());
|
||||||
compositeDisposable.add(
|
compositeDisposable.add(
|
||||||
Observable.fromCallable(() -> Utility.checksum(storage, id))
|
Observable.fromCallable(() -> Utility.checksum(storage, id))
|
||||||
.subscribeOn(Schedulers.computation())
|
.subscribeOn(Schedulers.computation())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(result -> {
|
.subscribe(result -> {
|
||||||
ShareUtils.copyToClipboard(mContext, result);
|
ShareUtils.copyToClipboard(mContext, result);
|
||||||
notificationManager.cancel(HASH_NOTIFICATION_ID);
|
notificationManager.cancel(HASH_NOTIFICATION_ID);
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
case R.id.source:
|
} else if (id == R.id.source) {
|
||||||
/*Intent intent = new Intent(Intent.ACTION_VIEW, Uri.parse(h.item.mission.source));
|
try {
|
||||||
mContext.startActivity(intent);*/
|
Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source);
|
||||||
try {
|
intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
|
||||||
Intent intent = NavigationHelper.getIntentByLink(mContext, h.item.mission.source);
|
mContext.startActivity(intent);
|
||||||
intent.addFlags(Intent.FLAG_ACTIVITY_PREVIOUS_IS_TOP);
|
} catch (Exception e) {
|
||||||
mContext.startActivity(intent);
|
Log.w(TAG, "Selected item has a invalid source", e);
|
||||||
} catch (Exception e) {
|
}
|
||||||
Log.w(TAG, "Selected item has a invalid source", e);
|
return true;
|
||||||
}
|
|
||||||
return true;
|
|
||||||
default:
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void applyChanges() {
|
public void applyChanges() {
|
||||||
|
|||||||
@@ -186,23 +186,24 @@ public class MissionsFragment extends Fragment {
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
int itemId = item.getItemId();
|
||||||
case R.id.switch_mode:
|
if (itemId == R.id.switch_mode) {
|
||||||
mLinear = !mLinear;
|
mLinear = !mLinear;
|
||||||
updateList();
|
updateList();
|
||||||
return true;
|
return true;
|
||||||
case R.id.clear_list:
|
} else if (itemId == R.id.clear_list) {
|
||||||
showClearDownloadHistoryPrompt();
|
showClearDownloadHistoryPrompt();
|
||||||
return true;
|
return true;
|
||||||
case R.id.start_downloads:
|
} else if (itemId == R.id.start_downloads) {
|
||||||
mBinder.getDownloadManager().startAllMissions();
|
mBinder.getDownloadManager().startAllMissions();
|
||||||
return true;
|
return true;
|
||||||
case R.id.pause_downloads:
|
} else if (itemId == R.id.pause_downloads) {
|
||||||
mBinder.getDownloadManager().pauseAllMissions(false);
|
mBinder.getDownloadManager().pauseAllMissions(false);
|
||||||
mAdapter.refreshMissionItems();// update items view
|
mAdapter.refreshMissionItems();// update items view
|
||||||
default:
|
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void showClearDownloadHistoryPrompt() {
|
public void showClearDownloadHistoryPrompt() {
|
||||||
|
|||||||
@@ -5,6 +5,6 @@
|
|||||||
android:viewportWidth="24"
|
android:viewportWidth="24"
|
||||||
android:tint="@color/defaultIconTint">
|
android:tint="@color/defaultIconTint">
|
||||||
<path
|
<path
|
||||||
android:pathData="M20 4H4c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zM9.5 16.5v-9l7 4.5-7 4.5z"
|
android:pathData="M20 4H4c-1.1 0-2 0.9-2 2v12c0 1.1 0.9 2 2 2h16c1.1 0 2-0.9 2-2V6c0-1.1-0.9-2-2-2zM9.5 16.5v-9l7 4.5-7 4.5z"
|
||||||
android:fillColor="#FF000000"/>
|
android:fillColor="#FF000000"/>
|
||||||
</vector>
|
</vector>
|
||||||
|
|||||||
@@ -122,8 +122,8 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/exo_controls_rewind"
|
android:src="@drawable/exo_controls_rewind"
|
||||||
android:tint="?attr/colorAccent"
|
android:contentDescription="@string/rewind"
|
||||||
android:contentDescription="@string/rewind" />
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/control_play_pause"
|
android:id="@+id/control_play_pause"
|
||||||
@@ -139,8 +139,8 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/ic_pause"
|
android:src="@drawable/ic_pause"
|
||||||
android:tint="?attr/colorAccent"
|
android:contentDescription="@string/pause"
|
||||||
android:contentDescription="@string/pause" />
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/control_progress_bar"
|
android:id="@+id/control_progress_bar"
|
||||||
@@ -172,8 +172,8 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/exo_controls_fastforward"
|
android:src="@drawable/exo_controls_fastforward"
|
||||||
android:tint="?attr/colorAccent"
|
android:contentDescription="@string/forward"
|
||||||
android:contentDescription="@string/forward" />
|
app:tint="?attr/colorAccent" />
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|
||||||
<RelativeLayout
|
<RelativeLayout
|
||||||
@@ -215,8 +215,8 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_repeat"
|
android:src="@drawable/ic_repeat"
|
||||||
android:tint="?attr/colorAccent"
|
android:contentDescription="@string/notification_action_repeat"
|
||||||
android:contentDescription="@string/notification_action_repeat" />
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
android:id="@+id/anchor"
|
android:id="@+id/anchor"
|
||||||
@@ -236,8 +236,8 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_shuffle"
|
android:src="@drawable/ic_shuffle"
|
||||||
android:tint="?attr/colorAccent"
|
android:contentDescription="@string/notification_action_shuffle"
|
||||||
android:contentDescription="@string/notification_action_shuffle" />
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/control_forward"
|
android:id="@+id/control_forward"
|
||||||
|
|||||||
@@ -175,7 +175,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_repeat"
|
android:src="@drawable/ic_repeat"
|
||||||
android:tint="?attr/colorAccent"
|
app:tint="?attr/colorAccent"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
@@ -205,7 +205,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/exo_controls_rewind"
|
android:src="@drawable/exo_controls_rewind"
|
||||||
android:tint="?attr/colorAccent" />
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/control_play_pause"
|
android:id="@+id/control_play_pause"
|
||||||
@@ -220,7 +220,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/ic_pause"
|
android:src="@drawable/ic_pause"
|
||||||
android:tint="?attr/colorAccent"
|
app:tint="?attr/colorAccent"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitCenter"
|
android:scaleType="fitCenter"
|
||||||
android:src="@drawable/exo_controls_fastforward"
|
android:src="@drawable/exo_controls_fastforward"
|
||||||
android:tint="?attr/colorAccent" />
|
app:tint="?attr/colorAccent" />
|
||||||
|
|
||||||
<androidx.appcompat.widget.AppCompatImageButton
|
<androidx.appcompat.widget.AppCompatImageButton
|
||||||
android:id="@+id/control_forward"
|
android:id="@+id/control_forward"
|
||||||
@@ -285,7 +285,7 @@
|
|||||||
android:focusable="true"
|
android:focusable="true"
|
||||||
android:scaleType="fitXY"
|
android:scaleType="fitXY"
|
||||||
android:src="@drawable/ic_shuffle"
|
android:src="@drawable/ic_shuffle"
|
||||||
android:tint="?attr/colorAccent"
|
app:tint="?attr/colorAccent"
|
||||||
tools:ignore="ContentDescription" />
|
tools:ignore="ContentDescription" />
|
||||||
|
|
||||||
</RelativeLayout>
|
</RelativeLayout>
|
||||||
|
|||||||
@@ -309,6 +309,6 @@
|
|||||||
<string name="notification_action_nothing">কিছু না</string>
|
<string name="notification_action_nothing">কিছু না</string>
|
||||||
<string name="yes">হ্যাঁ</string>
|
<string name="yes">হ্যাঁ</string>
|
||||||
<string name="no">না</string>
|
<string name="no">না</string>
|
||||||
<string name="search_with_service_name">সার্চ</string>
|
<string name="search_with_service_name">সার্চ %1$s</string>
|
||||||
<string name="search_with_service_name_and_filter">খুঁজুন</string>
|
<string name="search_with_service_name_and_filter">খুঁজুন %1$s (%2$s)</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
android.nonFinalResIds=false
|
|
||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
org.gradle.jvmargs=-Xmx2048M --add-opens jdk.compiler/com.sun.tools.javac.model=ALL-UNNAMED
|
||||||
systemProp.file.encoding=utf-8
|
systemProp.file.encoding=utf-8
|
||||||
|
|||||||
Reference in New Issue
Block a user