Merge remote-tracking branch 'origin/dev' into dev
This commit is contained in:
commit
f7f86a2f62
16 changed files with 537 additions and 246 deletions
|
@ -73,6 +73,31 @@ public class Downloader implements org.schabi.newpipe.extractor.Downloader {
|
||||||
mCookies = cookies;
|
mCookies = cookies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the size of the content that the url is pointing by firing a HEAD request.
|
||||||
|
*
|
||||||
|
* @param url an url pointing to the content
|
||||||
|
* @return the size of the content, in bytes
|
||||||
|
*/
|
||||||
|
public long getContentLength(String url) throws IOException {
|
||||||
|
Response response = null;
|
||||||
|
try {
|
||||||
|
final Request request = new Request.Builder()
|
||||||
|
.head().url(url)
|
||||||
|
.addHeader("User-Agent", USER_AGENT)
|
||||||
|
.build();
|
||||||
|
response = client.newCall(request).execute();
|
||||||
|
|
||||||
|
return Long.parseLong(response.header("Content-Length"));
|
||||||
|
} catch (NumberFormatException e) {
|
||||||
|
throw new IOException("Invalid content length", e);
|
||||||
|
} finally {
|
||||||
|
if (response != null) {
|
||||||
|
response.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download the text file at the supplied URL as in download(String),
|
* Download the text file at the supplied URL as in download(String),
|
||||||
* but set the HTTP header field "Accept-Language" to the supplied string.
|
* but set the HTTP header field "Accept-Language" to the supplied string.
|
||||||
|
|
|
@ -22,11 +22,13 @@ package org.schabi.newpipe;
|
||||||
|
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
|
import android.content.pm.PackageManager;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Looper;
|
import android.os.Looper;
|
||||||
import android.preference.PreferenceManager;
|
import android.preference.PreferenceManager;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.design.widget.NavigationView;
|
import android.support.design.widget.NavigationView;
|
||||||
import android.support.v4.app.Fragment;
|
import android.support.v4.app.Fragment;
|
||||||
import android.support.v4.view.GravityCompat;
|
import android.support.v4.view.GravityCompat;
|
||||||
|
@ -54,6 +56,7 @@ import org.schabi.newpipe.fragments.list.search.SearchFragment;
|
||||||
import org.schabi.newpipe.report.ErrorActivity;
|
import org.schabi.newpipe.report.ErrorActivity;
|
||||||
import org.schabi.newpipe.util.Constants;
|
import org.schabi.newpipe.util.Constants;
|
||||||
import org.schabi.newpipe.util.NavigationHelper;
|
import org.schabi.newpipe.util.NavigationHelper;
|
||||||
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ServiceHelper;
|
import org.schabi.newpipe.util.ServiceHelper;
|
||||||
import org.schabi.newpipe.util.StateSaver;
|
import org.schabi.newpipe.util.StateSaver;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
@ -94,10 +97,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
drawerItems = findViewById(R.id.navigation);
|
drawerItems = findViewById(R.id.navigation);
|
||||||
|
|
||||||
for(StreamingService s : NewPipe.getServices()) {
|
for(StreamingService s : NewPipe.getServices()) {
|
||||||
String title =
|
final String title = s.getServiceInfo().getName() +
|
||||||
s.getServiceInfo().getName() +
|
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
||||||
(ServiceHelper.isBeta(s) ? " (beta)" : "");
|
final MenuItem item = drawerItems.getMenu()
|
||||||
MenuItem item = drawerItems.getMenu()
|
|
||||||
.add(R.id.menu_services_group, s.getServiceId(), 0, title);
|
.add(R.id.menu_services_group, s.getServiceId(), 0, title);
|
||||||
item.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
item.setIcon(ServiceHelper.getIcon(s.getServiceId()));
|
||||||
}
|
}
|
||||||
|
@ -233,6 +235,26 @@ public class MainActivity extends AppCompatActivity {
|
||||||
} else super.onBackPressed();
|
} else super.onBackPressed();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
|
||||||
|
for (int i: grantResults){
|
||||||
|
if (i == PackageManager.PERMISSION_DENIED){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch (requestCode) {
|
||||||
|
case PermissionHelper.DOWNLOADS_REQUEST_CODE:
|
||||||
|
NavigationHelper.openDownloads(this);
|
||||||
|
break;
|
||||||
|
case PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE:
|
||||||
|
Fragment fragment = getSupportFragmentManager().findFragmentById(R.id.fragment_holder);
|
||||||
|
if (fragment instanceof VideoDetailFragment) {
|
||||||
|
((VideoDetailFragment) fragment).openDownloadDialog();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Implement the following diagram behavior for the up button:
|
* Implement the following diagram behavior for the up button:
|
||||||
* <pre>
|
* <pre>
|
||||||
|
@ -313,6 +335,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
case R.id.action_about:
|
case R.id.action_about:
|
||||||
NavigationHelper.openAbout(this);
|
NavigationHelper.openAbout(this);
|
||||||
return true;
|
return true;
|
||||||
|
case R.id.action_history:
|
||||||
|
NavigationHelper.openHistory(this);
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
return super.onOptionsItemSelected(item);
|
return super.onOptionsItemSelected(item);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,56 +1,61 @@
|
||||||
package org.schabi.newpipe.download;
|
package org.schabi.newpipe.download;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.support.annotation.IdRes;
|
import android.support.annotation.IdRes;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
import android.support.annotation.Nullable;
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.app.DialogFragment;
|
import android.support.v4.app.DialogFragment;
|
||||||
import android.support.v7.widget.Toolbar;
|
import android.support.v7.widget.Toolbar;
|
||||||
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;
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.EditText;
|
import android.widget.EditText;
|
||||||
import android.widget.RadioButton;
|
import android.widget.RadioButton;
|
||||||
import android.widget.RadioGroup;
|
import android.widget.RadioGroup;
|
||||||
import android.widget.SeekBar;
|
import android.widget.SeekBar;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import org.schabi.newpipe.MainActivity;
|
import org.schabi.newpipe.MainActivity;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
|
||||||
import org.schabi.newpipe.extractor.stream.AudioStream;
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
import org.schabi.newpipe.extractor.stream.StreamInfo;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.detail.SpinnerToolbarAdapter;
|
|
||||||
import org.schabi.newpipe.settings.NewPipeSettings;
|
import org.schabi.newpipe.settings.NewPipeSettings;
|
||||||
import org.schabi.newpipe.util.FilenameUtils;
|
import org.schabi.newpipe.util.FilenameUtils;
|
||||||
import org.schabi.newpipe.util.ListHelper;
|
import org.schabi.newpipe.util.ListHelper;
|
||||||
import org.schabi.newpipe.util.PermissionHelper;
|
import org.schabi.newpipe.util.PermissionHelper;
|
||||||
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.Serializable;
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import icepick.Icepick;
|
||||||
|
import icepick.State;
|
||||||
|
import io.reactivex.disposables.CompositeDisposable;
|
||||||
import us.shandian.giga.service.DownloadManagerService;
|
import us.shandian.giga.service.DownloadManagerService;
|
||||||
|
|
||||||
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheckedChangeListener, AdapterView.OnItemSelectedListener {
|
||||||
private static final String TAG = "DialogFragment";
|
private static final String TAG = "DialogFragment";
|
||||||
private static final boolean DEBUG = MainActivity.DEBUG;
|
private static final boolean DEBUG = MainActivity.DEBUG;
|
||||||
|
|
||||||
private static final String INFO_KEY = "info_key";
|
@State protected StreamInfo currentInfo;
|
||||||
private static final String SORTED_VIDEOS_LIST_KEY = "sorted_videos_list_key";
|
@State protected StreamSizeWrapper<AudioStream> wrappedAudioStreams = StreamSizeWrapper.empty();
|
||||||
private static final String SELECTED_VIDEO_KEY = "selected_video_key";
|
@State protected StreamSizeWrapper<VideoStream> wrappedVideoStreams = StreamSizeWrapper.empty();
|
||||||
private static final String SELECTED_AUDIO_KEY = "selected_audio_key";
|
@State protected int selectedVideoIndex = 0;
|
||||||
|
@State protected int selectedAudioIndex = 0;
|
||||||
|
|
||||||
private StreamInfo currentInfo;
|
private StreamItemAdapter<AudioStream> audioStreamsAdapter;
|
||||||
private ArrayList<VideoStream> sortedStreamVideosList;
|
private StreamItemAdapter<VideoStream> videoStreamsAdapter;
|
||||||
private int selectedVideoIndex;
|
|
||||||
private int selectedAudioIndex;
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private EditText nameEditText;
|
private EditText nameEditText;
|
||||||
private Spinner streamsSpinner;
|
private Spinner streamsSpinner;
|
||||||
|
@ -58,17 +63,50 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
private TextView threadsCountTextView;
|
private TextView threadsCountTextView;
|
||||||
private SeekBar threadsSeekBar;
|
private SeekBar threadsSeekBar;
|
||||||
|
|
||||||
public static DownloadDialog newInstance(StreamInfo info, ArrayList<VideoStream> sortedStreamVideosList, int selectedVideoIndex) {
|
public static DownloadDialog newInstance(StreamInfo info) {
|
||||||
DownloadDialog dialog = new DownloadDialog();
|
DownloadDialog dialog = new DownloadDialog();
|
||||||
dialog.setInfo(info, sortedStreamVideosList, selectedVideoIndex);
|
dialog.setInfo(info);
|
||||||
dialog.setStyle(DialogFragment.STYLE_NO_TITLE, 0);
|
|
||||||
return dialog;
|
return dialog;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setInfo(StreamInfo info, ArrayList<VideoStream> sortedStreamVideosList, int selectedVideoIndex) {
|
public static DownloadDialog newInstance(Context context, StreamInfo info) {
|
||||||
|
final ArrayList<VideoStream> streamsList = new ArrayList<>(ListHelper.getSortedStreamVideosList(context,
|
||||||
|
info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
||||||
|
final int selectedStreamIndex = ListHelper.getDefaultResolutionIndex(context, streamsList);
|
||||||
|
|
||||||
|
final DownloadDialog instance = newInstance(info);
|
||||||
|
instance.setVideoStreams(streamsList);
|
||||||
|
instance.setSelectedVideoStream(selectedStreamIndex);
|
||||||
|
instance.setAudioStreams(info.getAudioStreams());
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setInfo(StreamInfo info) {
|
||||||
this.currentInfo = info;
|
this.currentInfo = info;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioStreams(List<AudioStream> audioStreams) {
|
||||||
|
setAudioStreams(new StreamSizeWrapper<>(audioStreams));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAudioStreams(StreamSizeWrapper<AudioStream> wrappedAudioStreams) {
|
||||||
|
this.wrappedAudioStreams = wrappedAudioStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVideoStreams(List<VideoStream> videoStreams) {
|
||||||
|
setVideoStreams(new StreamSizeWrapper<>(videoStreams));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setVideoStreams(StreamSizeWrapper<VideoStream> wrappedVideoStreams) {
|
||||||
|
this.wrappedVideoStreams = wrappedVideoStreams;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSelectedVideoStream(int selectedVideoIndex) {
|
||||||
this.selectedVideoIndex = selectedVideoIndex;
|
this.selectedVideoIndex = selectedVideoIndex;
|
||||||
this.sortedStreamVideosList = sortedStreamVideosList;
|
}
|
||||||
|
|
||||||
|
public void setSelectedAudioStream(int selectedAudioIndex) {
|
||||||
|
this.selectedAudioIndex = selectedAudioIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -79,33 +117,26 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
if (DEBUG) Log.d(TAG, "onCreate() called with: savedInstanceState = [" + savedInstanceState + "]");
|
||||||
if (!PermissionHelper.checkStoragePermissions(getActivity())) {
|
if (!PermissionHelper.checkStoragePermissions(getActivity(), PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
getDialog().dismiss();
|
getDialog().dismiss();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
setStyle(STYLE_NO_TITLE, ThemeHelper.getDialogTheme(getContext()));
|
||||||
Serializable serial = savedInstanceState.getSerializable(INFO_KEY);
|
Icepick.restoreInstanceState(this, savedInstanceState);
|
||||||
if (serial instanceof StreamInfo) currentInfo = (StreamInfo) serial;
|
|
||||||
|
|
||||||
serial = savedInstanceState.getSerializable(SORTED_VIDEOS_LIST_KEY);
|
this.videoStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedVideoStreams, true);
|
||||||
if (serial instanceof ArrayList) { //noinspection unchecked
|
this.audioStreamsAdapter = new StreamItemAdapter<>(getContext(), wrappedAudioStreams);
|
||||||
sortedStreamVideosList = (ArrayList<VideoStream>) serial;
|
|
||||||
}
|
|
||||||
|
|
||||||
selectedVideoIndex = savedInstanceState.getInt(SELECTED_VIDEO_KEY, 0);
|
|
||||||
selectedAudioIndex = savedInstanceState.getInt(SELECTED_AUDIO_KEY, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||||
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
if (DEBUG) Log.d(TAG, "onCreateView() called with: inflater = [" + inflater + "], container = [" + container + "], savedInstanceState = [" + savedInstanceState + "]");
|
||||||
return inflater.inflate(R.layout.dialog_url, container);
|
return inflater.inflate(R.layout.download_dialog, container);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onViewCreated(View view, @Nullable Bundle savedInstanceState) {
|
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
|
||||||
super.onViewCreated(view, savedInstanceState);
|
super.onViewCreated(view, savedInstanceState);
|
||||||
nameEditText = view.findViewById(R.id.file_name);
|
nameEditText = view.findViewById(R.id.file_name);
|
||||||
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
nameEditText.setText(FilenameUtils.createFilename(getContext(), currentInfo.getName()));
|
||||||
|
@ -116,12 +147,12 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
|
|
||||||
threadsCountTextView = view.findViewById(R.id.threads_count);
|
threadsCountTextView = view.findViewById(R.id.threads_count);
|
||||||
threadsSeekBar = view.findViewById(R.id.threads);
|
threadsSeekBar = view.findViewById(R.id.threads);
|
||||||
|
|
||||||
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
radioVideoAudioGroup = view.findViewById(R.id.video_audio_group);
|
||||||
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
radioVideoAudioGroup.setOnCheckedChangeListener(this);
|
||||||
|
|
||||||
initToolbar(view.<Toolbar>findViewById(R.id.toolbar));
|
initToolbar(view.findViewById(R.id.toolbar));
|
||||||
checkDownloadOptions(view);
|
setupDownloadOptions();
|
||||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
|
||||||
|
|
||||||
int def = 3;
|
int def = 3;
|
||||||
threadsCountTextView.setText(String.valueOf(def));
|
threadsCountTextView.setText(String.valueOf(def));
|
||||||
|
@ -141,15 +172,35 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
public void onStopTrackingTouch(SeekBar p1) {
|
public void onStopTrackingTouch(SeekBar p1) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
fetchStreamsSize();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void fetchStreamsSize() {
|
||||||
|
disposables.clear();
|
||||||
|
|
||||||
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedVideoStreams).subscribe(result -> {
|
||||||
|
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.video_button) {
|
||||||
|
setupVideoSpinner();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
disposables.add(StreamSizeWrapper.fetchSizeForWrapper(wrappedAudioStreams).subscribe(result -> {
|
||||||
|
if (radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button) {
|
||||||
|
setupAudioSpinner();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onDestroy() {
|
||||||
|
super.onDestroy();
|
||||||
|
disposables.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(Bundle outState) {
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putSerializable(INFO_KEY, currentInfo);
|
Icepick.saveInstanceState(this, outState);
|
||||||
outState.putSerializable(SORTED_VIDEOS_LIST_KEY, sortedStreamVideosList);
|
|
||||||
outState.putInt(SELECTED_VIDEO_KEY, selectedVideoIndex);
|
|
||||||
outState.putInt(SELECTED_AUDIO_KEY, selectedAudioIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -161,39 +212,31 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
toolbar.setTitle(R.string.download_dialog_title);
|
toolbar.setTitle(R.string.download_dialog_title);
|
||||||
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
toolbar.setNavigationIcon(ThemeHelper.isLightThemeSelected(getActivity()) ? R.drawable.ic_arrow_back_black_24dp : R.drawable.ic_arrow_back_white_24dp);
|
||||||
toolbar.inflateMenu(R.menu.dialog_url);
|
toolbar.inflateMenu(R.menu.dialog_url);
|
||||||
toolbar.setNavigationOnClickListener(new View.OnClickListener() {
|
toolbar.setNavigationOnClickListener(v -> getDialog().dismiss());
|
||||||
@Override
|
|
||||||
public void onClick(View v) {
|
|
||||||
getDialog().dismiss();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
toolbar.setOnMenuItemClickListener(new Toolbar.OnMenuItemClickListener() {
|
toolbar.setOnMenuItemClickListener(item -> {
|
||||||
@Override
|
if (item.getItemId() == R.id.okay) {
|
||||||
public boolean onMenuItemClick(MenuItem item) {
|
downloadSelected();
|
||||||
if (item.getItemId() == R.id.okay) {
|
return true;
|
||||||
downloadSelected();
|
|
||||||
return true;
|
|
||||||
} else return false;
|
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setupAudioSpinner(final List<AudioStream> audioStreams, Spinner spinner) {
|
private void setupAudioSpinner() {
|
||||||
String[] items = new String[audioStreams.size()];
|
if (getContext() == null) return;
|
||||||
for (int i = 0; i < audioStreams.size(); i++) {
|
|
||||||
AudioStream audioStream = audioStreams.get(i);
|
|
||||||
items[i] = audioStream.getFormat().getName() + " " + audioStream.getAverageBitrate() + "kbps";
|
|
||||||
}
|
|
||||||
|
|
||||||
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items);
|
streamsSpinner.setAdapter(audioStreamsAdapter);
|
||||||
spinner.setAdapter(itemAdapter);
|
streamsSpinner.setSelection(selectedAudioIndex);
|
||||||
spinner.setSelection(selectedAudioIndex);
|
setRadioButtonsState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setupVideoSpinner(final List<VideoStream> videoStreams, Spinner spinner) {
|
private void setupVideoSpinner() {
|
||||||
spinner.setAdapter(new SpinnerToolbarAdapter(getContext(), videoStreams, true));
|
if (getContext() == null) return;
|
||||||
spinner.setSelection(selectedVideoIndex);
|
|
||||||
|
streamsSpinner.setAdapter(videoStreamsAdapter);
|
||||||
|
streamsSpinner.setSelection(selectedVideoIndex);
|
||||||
|
setRadioButtonsState(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -205,10 +248,10 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
if (DEBUG) Log.d(TAG, "onCheckedChanged() called with: group = [" + group + "], checkedId = [" + checkedId + "]");
|
||||||
switch (checkedId) {
|
switch (checkedId) {
|
||||||
case R.id.audio_button:
|
case R.id.audio_button:
|
||||||
setupAudioSpinner(currentInfo.getAudioStreams(), streamsSpinner);
|
setupAudioSpinner();
|
||||||
break;
|
break;
|
||||||
case R.id.video_button:
|
case R.id.video_button:
|
||||||
setupVideoSpinner(sortedStreamVideosList, streamsSpinner);
|
setupVideoSpinner();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -238,37 +281,53 @@ public class DownloadDialog extends DialogFragment implements RadioGroup.OnCheck
|
||||||
// Utils
|
// Utils
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
||||||
protected void checkDownloadOptions(View view) {
|
protected void setupDownloadOptions() {
|
||||||
RadioButton audioButton = view.findViewById(R.id.audio_button);
|
setRadioButtonsState(false);
|
||||||
RadioButton videoButton = view.findViewById(R.id.video_button);
|
|
||||||
|
|
||||||
if (currentInfo.getAudioStreams() == null || currentInfo.getAudioStreams().size() == 0) {
|
final RadioButton audioButton = radioVideoAudioGroup.findViewById(R.id.audio_button);
|
||||||
audioButton.setVisibility(View.GONE);
|
final RadioButton videoButton = radioVideoAudioGroup.findViewById(R.id.video_button);
|
||||||
|
final boolean isVideoStreamsAvailable = videoStreamsAdapter.getCount() > 0;
|
||||||
|
final boolean isAudioStreamsAvailable = audioStreamsAdapter.getCount() > 0;
|
||||||
|
|
||||||
|
audioButton.setVisibility(isAudioStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||||
|
videoButton.setVisibility(isVideoStreamsAvailable ? View.VISIBLE : View.GONE);
|
||||||
|
|
||||||
|
if (isVideoStreamsAvailable) {
|
||||||
videoButton.setChecked(true);
|
videoButton.setChecked(true);
|
||||||
} else if (sortedStreamVideosList == null || sortedStreamVideosList.size() == 0) {
|
setupVideoSpinner();
|
||||||
videoButton.setVisibility(View.GONE);
|
} else if (isAudioStreamsAvailable) {
|
||||||
audioButton.setChecked(true);
|
audioButton.setChecked(true);
|
||||||
|
setupAudioSpinner();
|
||||||
|
} else {
|
||||||
|
Toast.makeText(getContext(), R.string.no_streams_available_download, Toast.LENGTH_SHORT).show();
|
||||||
|
getDialog().dismiss();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setRadioButtonsState(boolean enabled) {
|
||||||
|
radioVideoAudioGroup.findViewById(R.id.audio_button).setEnabled(enabled);
|
||||||
|
radioVideoAudioGroup.findViewById(R.id.video_button).setEnabled(enabled);
|
||||||
|
}
|
||||||
|
|
||||||
private void downloadSelected() {
|
private void downloadSelected() {
|
||||||
String url, location;
|
Stream stream;
|
||||||
|
String location;
|
||||||
|
|
||||||
String fileName = nameEditText.getText().toString().trim();
|
String fileName = nameEditText.getText().toString().trim();
|
||||||
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
|
if (fileName.isEmpty()) fileName = FilenameUtils.createFilename(getContext(), currentInfo.getName());
|
||||||
|
|
||||||
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
boolean isAudio = radioVideoAudioGroup.getCheckedRadioButtonId() == R.id.audio_button;
|
||||||
if (isAudio) {
|
if (isAudio) {
|
||||||
url = currentInfo.getAudioStreams().get(selectedAudioIndex).getUrl();
|
stream = audioStreamsAdapter.getItem(selectedAudioIndex);
|
||||||
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
location = NewPipeSettings.getAudioDownloadPath(getContext());
|
||||||
fileName += "." + currentInfo.getAudioStreams().get(selectedAudioIndex).getFormat().getSuffix();
|
|
||||||
} else {
|
} else {
|
||||||
url = sortedStreamVideosList.get(selectedVideoIndex).getUrl();
|
stream = videoStreamsAdapter.getItem(selectedVideoIndex);
|
||||||
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
location = NewPipeSettings.getVideoDownloadPath(getContext());
|
||||||
fileName += "." + sortedStreamVideosList.get(selectedVideoIndex).getFormat().getSuffix();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String url = stream.getUrl();
|
||||||
|
fileName += "." + stream.getFormat().getSuffix();
|
||||||
|
|
||||||
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
|
DownloadManagerService.startMission(getContext(), url, location, fileName, isAudio, threadsSeekBar.getProgress() + 1);
|
||||||
getDialog().dismiss();
|
getDialog().dismiss();
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,74 +0,0 @@
|
||||||
package org.schabi.newpipe.fragments.detail;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.view.LayoutInflater;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.BaseAdapter;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import org.schabi.newpipe.R;
|
|
||||||
import org.schabi.newpipe.extractor.MediaFormat;
|
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
public class SpinnerToolbarAdapter extends BaseAdapter {
|
|
||||||
private final List<VideoStream> videoStreams;
|
|
||||||
private final boolean showIconNoAudio;
|
|
||||||
|
|
||||||
private final Context context;
|
|
||||||
|
|
||||||
public SpinnerToolbarAdapter(Context context, List<VideoStream> videoStreams, boolean showIconNoAudio) {
|
|
||||||
this.context = context;
|
|
||||||
this.videoStreams = videoStreams;
|
|
||||||
this.showIconNoAudio = showIconNoAudio;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getCount() {
|
|
||||||
return videoStreams.size();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Object getItem(int position) {
|
|
||||||
return videoStreams.get(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public long getItemId(int position) {
|
|
||||||
return position;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
|
||||||
return getCustomView(position, convertView, parent, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public View getView(int position, View convertView, ViewGroup parent) {
|
|
||||||
return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) {
|
|
||||||
if (convertView == null) {
|
|
||||||
convertView = LayoutInflater.from(context).inflate(R.layout.resolutions_spinner_item, parent, false);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImageView woSoundIcon = convertView.findViewById(R.id.wo_sound_icon);
|
|
||||||
TextView text = convertView.findViewById(android.R.id.text1);
|
|
||||||
VideoStream item = (VideoStream) getItem(position);
|
|
||||||
text.setText(item.getFormat().getName() + " " + item.getResolution());
|
|
||||||
|
|
||||||
int visibility = !showIconNoAudio ? View.GONE
|
|
||||||
: item.isVideoOnly ? View.VISIBLE
|
|
||||||
: isDropdownItem ? View.INVISIBLE
|
|
||||||
: View.GONE;
|
|
||||||
woSoundIcon.setVisibility(visibility);
|
|
||||||
|
|
||||||
return convertView;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -12,6 +12,7 @@ import android.preference.PreferenceManager;
|
||||||
import android.support.annotation.DrawableRes;
|
import android.support.annotation.DrawableRes;
|
||||||
import android.support.annotation.FloatRange;
|
import android.support.annotation.FloatRange;
|
||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
import android.support.annotation.Nullable;
|
||||||
import android.support.v4.content.ContextCompat;
|
import android.support.v4.content.ContextCompat;
|
||||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||||
import android.support.v7.app.ActionBar;
|
import android.support.v7.app.ActionBar;
|
||||||
|
@ -61,6 +62,8 @@ import org.schabi.newpipe.extractor.stream.StreamType;
|
||||||
import org.schabi.newpipe.extractor.stream.VideoStream;
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
import org.schabi.newpipe.fragments.BackPressable;
|
import org.schabi.newpipe.fragments.BackPressable;
|
||||||
import org.schabi.newpipe.fragments.BaseStateFragment;
|
import org.schabi.newpipe.fragments.BaseStateFragment;
|
||||||
|
import org.schabi.newpipe.util.StreamItemAdapter;
|
||||||
|
import org.schabi.newpipe.util.StreamItemAdapter.StreamSizeWrapper;
|
||||||
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
import org.schabi.newpipe.fragments.local.dialog.PlaylistAppendDialog;
|
||||||
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
import org.schabi.newpipe.info_list.InfoItemBuilder;
|
||||||
import org.schabi.newpipe.info_list.InfoItemDialog;
|
import org.schabi.newpipe.info_list.InfoItemDialog;
|
||||||
|
@ -83,9 +86,9 @@ import org.schabi.newpipe.util.PermissionHelper;
|
||||||
import org.schabi.newpipe.util.ThemeHelper;
|
import org.schabi.newpipe.util.ThemeHelper;
|
||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.LinkedList;
|
import java.util.LinkedList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import io.reactivex.Single;
|
import io.reactivex.Single;
|
||||||
|
@ -107,8 +110,6 @@ public class VideoDetailFragment
|
||||||
// Amount of videos to show on start
|
// Amount of videos to show on start
|
||||||
private static final int INITIAL_RELATED_VIDEOS = 8;
|
private static final int INITIAL_RELATED_VIDEOS = 8;
|
||||||
|
|
||||||
private ArrayList<VideoStream> sortedStreamVideosList;
|
|
||||||
|
|
||||||
private InfoItemBuilder infoItemBuilder = null;
|
private InfoItemBuilder infoItemBuilder = null;
|
||||||
|
|
||||||
private int updateFlags = 0;
|
private int updateFlags = 0;
|
||||||
|
@ -120,18 +121,16 @@ public class VideoDetailFragment
|
||||||
private boolean showRelatedStreams;
|
private boolean showRelatedStreams;
|
||||||
private boolean wasRelatedStreamsExpanded = false;
|
private boolean wasRelatedStreamsExpanded = false;
|
||||||
|
|
||||||
@State
|
@State protected int serviceId = Constants.NO_SERVICE_ID;
|
||||||
protected int serviceId = Constants.NO_SERVICE_ID;
|
@State protected String name;
|
||||||
@State
|
@State protected String url;
|
||||||
protected String name;
|
|
||||||
@State
|
|
||||||
protected String url;
|
|
||||||
|
|
||||||
private StreamInfo currentInfo;
|
private StreamInfo currentInfo;
|
||||||
private Disposable currentWorker;
|
private Disposable currentWorker;
|
||||||
private CompositeDisposable disposables = new CompositeDisposable();
|
private CompositeDisposable disposables = new CompositeDisposable();
|
||||||
|
|
||||||
private int selectedVideoStream = -1;
|
private List<VideoStream> sortedVideoStreams;
|
||||||
|
private int selectedVideoStreamIndex = -1;
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Views
|
// Views
|
||||||
|
@ -355,21 +354,8 @@ public class VideoDetailFragment
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_controls_download:
|
case R.id.detail_controls_download:
|
||||||
if (!PermissionHelper.checkStoragePermissions(activity)) {
|
if (PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOAD_DIALOG_REQUEST_CODE)) {
|
||||||
return;
|
this.openDownloadDialog();
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
DownloadDialog downloadDialog =
|
|
||||||
DownloadDialog.newInstance(currentInfo,
|
|
||||||
sortedStreamVideosList,
|
|
||||||
selectedVideoStream);
|
|
||||||
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
|
||||||
} catch (Exception e) {
|
|
||||||
Toast.makeText(activity,
|
|
||||||
R.string.could_not_setup_download_menu,
|
|
||||||
Toast.LENGTH_LONG).show();
|
|
||||||
e.printStackTrace();
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case R.id.detail_uploader_root_layout:
|
case R.id.detail_uploader_root_layout:
|
||||||
|
@ -411,6 +397,9 @@ public class VideoDetailFragment
|
||||||
case R.id.detail_controls_popup:
|
case R.id.detail_controls_popup:
|
||||||
openPopupPlayer(true);
|
openPopupPlayer(true);
|
||||||
break;
|
break;
|
||||||
|
case R.id.detail_controls_download:
|
||||||
|
NavigationHelper.openDownloads(getActivity());
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -532,6 +521,7 @@ public class VideoDetailFragment
|
||||||
detailControlsPopup.setOnClickListener(this);
|
detailControlsPopup.setOnClickListener(this);
|
||||||
detailControlsAddToPlaylist.setOnClickListener(this);
|
detailControlsAddToPlaylist.setOnClickListener(this);
|
||||||
detailControlsDownload.setOnClickListener(this);
|
detailControlsDownload.setOnClickListener(this);
|
||||||
|
detailControlsDownload.setOnLongClickListener(this);
|
||||||
relatedStreamExpandButton.setOnClickListener(this);
|
relatedStreamExpandButton.setOnClickListener(this);
|
||||||
|
|
||||||
detailControlsBackground.setLongClickable(true);
|
detailControlsBackground.setLongClickable(true);
|
||||||
|
@ -721,27 +711,25 @@ public class VideoDetailFragment
|
||||||
|
|
||||||
private void setupActionBar(final StreamInfo info) {
|
private void setupActionBar(final StreamInfo info) {
|
||||||
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
|
if (DEBUG) Log.d(TAG, "setupActionBarHandler() called with: info = [" + info + "]");
|
||||||
sortedStreamVideosList = new ArrayList<>(ListHelper.getSortedStreamVideosList(
|
|
||||||
activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false));
|
|
||||||
|
|
||||||
selectedVideoStream = ListHelper.getDefaultResolutionIndex(activity, sortedStreamVideosList);
|
|
||||||
|
|
||||||
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
boolean isExternalPlayerEnabled = PreferenceManager.getDefaultSharedPreferences(activity)
|
||||||
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
.getBoolean(activity.getString(R.string.use_external_video_player_key), false);
|
||||||
spinnerToolbar.setAdapter(new SpinnerToolbarAdapter(activity, sortedStreamVideosList,
|
|
||||||
isExternalPlayerEnabled));
|
sortedVideoStreams = ListHelper.getSortedStreamVideosList(activity, info.getVideoStreams(), info.getVideoOnlyStreams(), false);
|
||||||
spinnerToolbar.setSelection(selectedVideoStream);
|
selectedVideoStreamIndex = ListHelper.getDefaultResolutionIndex(activity, sortedVideoStreams);
|
||||||
|
|
||||||
|
final StreamItemAdapter<VideoStream> streamsAdapter = new StreamItemAdapter<>(activity, new StreamSizeWrapper<>(sortedVideoStreams), isExternalPlayerEnabled);
|
||||||
|
spinnerToolbar.setAdapter(streamsAdapter);
|
||||||
|
spinnerToolbar.setSelection(selectedVideoStreamIndex);
|
||||||
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
spinnerToolbar.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||||
selectedVideoStream = position;
|
selectedVideoStreamIndex = position;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNothingSelected(AdapterView<?> parent) {
|
public void onNothingSelected(AdapterView<?> parent) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
|
@ -953,8 +941,9 @@ public class VideoDetailFragment
|
||||||
this.autoPlayEnabled = autoplay;
|
this.autoPlayEnabled = autoplay;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
private VideoStream getSelectedVideoStream() {
|
private VideoStream getSelectedVideoStream() {
|
||||||
return sortedStreamVideosList.get(selectedVideoStream);
|
return sortedVideoStreams != null ? sortedVideoStreams.get(selectedVideoStreamIndex) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void prepareDescription(final String descriptionHtml) {
|
private void prepareDescription(final String descriptionHtml) {
|
||||||
|
@ -1227,6 +1216,23 @@ public class VideoDetailFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public void openDownloadDialog() {
|
||||||
|
try {
|
||||||
|
DownloadDialog downloadDialog = DownloadDialog.newInstance(currentInfo);
|
||||||
|
downloadDialog.setVideoStreams(sortedVideoStreams);
|
||||||
|
downloadDialog.setAudioStreams(currentInfo.getAudioStreams());
|
||||||
|
downloadDialog.setSelectedVideoStream(selectedVideoStreamIndex);
|
||||||
|
|
||||||
|
downloadDialog.show(activity.getSupportFragmentManager(), "downloadDialog");
|
||||||
|
} catch (Exception e) {
|
||||||
|
Toast.makeText(activity,
|
||||||
|
R.string.could_not_setup_download_menu,
|
||||||
|
Toast.LENGTH_LONG).show();
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/*//////////////////////////////////////////////////////////////////////////
|
/*//////////////////////////////////////////////////////////////////////////
|
||||||
// Stream Results
|
// Stream Results
|
||||||
//////////////////////////////////////////////////////////////////////////*/
|
//////////////////////////////////////////////////////////////////////////*/
|
||||||
|
|
|
@ -428,7 +428,7 @@ public class NavigationHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
public static boolean openDownloads(Activity activity) {
|
public static boolean openDownloads(Activity activity) {
|
||||||
if (!PermissionHelper.checkStoragePermissions(activity)) {
|
if (!PermissionHelper.checkStoragePermissions(activity, PermissionHelper.DOWNLOADS_REQUEST_CODE)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Intent intent = new Intent(activity, DownloadActivity.class);
|
Intent intent = new Intent(activity, DownloadActivity.class);
|
||||||
|
|
|
@ -18,26 +18,26 @@ import android.widget.Toast;
|
||||||
import org.schabi.newpipe.R;
|
import org.schabi.newpipe.R;
|
||||||
|
|
||||||
public class PermissionHelper {
|
public class PermissionHelper {
|
||||||
public static final int PERMISSION_WRITE_STORAGE = 778;
|
public static final int DOWNLOAD_DIALOG_REQUEST_CODE = 778;
|
||||||
public static final int PERMISSION_READ_STORAGE = 777;
|
public static final int DOWNLOADS_REQUEST_CODE = 777;
|
||||||
|
|
||||||
|
|
||||||
public static boolean checkStoragePermissions(Activity activity) {
|
public static boolean checkStoragePermissions(Activity activity, int requestCode) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||||
if(!checkReadStoragePermissions(activity)) return false;
|
if(!checkReadStoragePermissions(activity, requestCode)) return false;
|
||||||
}
|
}
|
||||||
return checkWriteStoragePermissions(activity);
|
return checkWriteStoragePermissions(activity, requestCode);
|
||||||
}
|
}
|
||||||
|
|
||||||
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
@RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN)
|
||||||
public static boolean checkReadStoragePermissions(Activity activity) {
|
public static boolean checkReadStoragePermissions(Activity activity, int requestCode) {
|
||||||
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
if (ContextCompat.checkSelfPermission(activity, Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||||
!= PackageManager.PERMISSION_GRANTED) {
|
!= PackageManager.PERMISSION_GRANTED) {
|
||||||
ActivityCompat.requestPermissions(activity,
|
ActivityCompat.requestPermissions(activity,
|
||||||
new String[]{
|
new String[]{
|
||||||
Manifest.permission.READ_EXTERNAL_STORAGE,
|
Manifest.permission.READ_EXTERNAL_STORAGE,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
PERMISSION_READ_STORAGE);
|
requestCode);
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
@ -45,7 +45,7 @@ public class PermissionHelper {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public static boolean checkWriteStoragePermissions(Activity activity) {
|
public static boolean checkWriteStoragePermissions(Activity activity, int requestCode) {
|
||||||
// Here, thisActivity is the current activity
|
// Here, thisActivity is the current activity
|
||||||
if (ContextCompat.checkSelfPermission(activity,
|
if (ContextCompat.checkSelfPermission(activity,
|
||||||
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
Manifest.permission.WRITE_EXTERNAL_STORAGE)
|
||||||
|
@ -63,7 +63,7 @@ public class PermissionHelper {
|
||||||
// No explanation needed, we can request the permission.
|
// No explanation needed, we can request the permission.
|
||||||
ActivityCompat.requestPermissions(activity,
|
ActivityCompat.requestPermissions(activity,
|
||||||
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE},
|
||||||
PERMISSION_WRITE_STORAGE);
|
requestCode);
|
||||||
|
|
||||||
// PERMISSION_WRITE_STORAGE is an
|
// PERMISSION_WRITE_STORAGE is an
|
||||||
// app-defined int constant. The callback method gets the
|
// app-defined int constant. The callback method gets the
|
||||||
|
|
196
app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
Normal file
196
app/src/main/java/org/schabi/newpipe/util/StreamItemAdapter.java
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
package org.schabi.newpipe.util;
|
||||||
|
|
||||||
|
import android.content.Context;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.BaseAdapter;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
import android.widget.Spinner;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import org.schabi.newpipe.Downloader;
|
||||||
|
import org.schabi.newpipe.R;
|
||||||
|
import org.schabi.newpipe.extractor.stream.AudioStream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.Stream;
|
||||||
|
import org.schabi.newpipe.extractor.stream.VideoStream;
|
||||||
|
|
||||||
|
import java.io.Serializable;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.concurrent.Callable;
|
||||||
|
|
||||||
|
import io.reactivex.Single;
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers;
|
||||||
|
import io.reactivex.schedulers.Schedulers;
|
||||||
|
import us.shandian.giga.util.Utility;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A list adapter for a list of {@link Stream streams}, currently supporting {@link VideoStream} and {@link AudioStream}.
|
||||||
|
*/
|
||||||
|
public class StreamItemAdapter<T extends Stream> extends BaseAdapter {
|
||||||
|
private final Context context;
|
||||||
|
|
||||||
|
private StreamSizeWrapper<T> streamsWrapper;
|
||||||
|
private final boolean showIconNoAudio;
|
||||||
|
|
||||||
|
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper, boolean showIconNoAudio) {
|
||||||
|
this.context = context;
|
||||||
|
this.streamsWrapper = streamsWrapper;
|
||||||
|
this.showIconNoAudio = showIconNoAudio;
|
||||||
|
}
|
||||||
|
|
||||||
|
public StreamItemAdapter(Context context, StreamSizeWrapper<T> streamsWrapper) {
|
||||||
|
this(context, streamsWrapper, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> getAll() {
|
||||||
|
return streamsWrapper.getStreamsList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getCount() {
|
||||||
|
return streamsWrapper.getStreamsList().size();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public T getItem(int position) {
|
||||||
|
return streamsWrapper.getStreamsList().get(position);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
return position;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getDropDownView(int position, View convertView, ViewGroup parent) {
|
||||||
|
return getCustomView(position, convertView, parent, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View getView(int position, View convertView, ViewGroup parent) {
|
||||||
|
return getCustomView(((Spinner) parent).getSelectedItemPosition(), convertView, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private View getCustomView(int position, View convertView, ViewGroup parent, boolean isDropdownItem) {
|
||||||
|
if (convertView == null) {
|
||||||
|
convertView = LayoutInflater.from(context).inflate(R.layout.stream_quality_item, parent, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
final ImageView woSoundIconView = convertView.findViewById(R.id.wo_sound_icon);
|
||||||
|
final TextView formatNameView = convertView.findViewById(R.id.stream_format_name);
|
||||||
|
final TextView qualityView = convertView.findViewById(R.id.stream_quality);
|
||||||
|
final TextView sizeView = convertView.findViewById(R.id.stream_size);
|
||||||
|
|
||||||
|
final T stream = getItem(position);
|
||||||
|
|
||||||
|
int woSoundIconVisibility = View.GONE;
|
||||||
|
String qualityString;
|
||||||
|
|
||||||
|
if (stream instanceof VideoStream) {
|
||||||
|
qualityString = ((VideoStream) stream).getResolution();
|
||||||
|
|
||||||
|
if (!showIconNoAudio) {
|
||||||
|
woSoundIconVisibility = View.GONE;
|
||||||
|
} else if (((VideoStream) stream).isVideoOnly()) {
|
||||||
|
woSoundIconVisibility = View.VISIBLE;
|
||||||
|
} else if (isDropdownItem) {
|
||||||
|
woSoundIconVisibility = View.INVISIBLE;
|
||||||
|
}
|
||||||
|
} else if (stream instanceof AudioStream) {
|
||||||
|
qualityString = ((AudioStream) stream).getAverageBitrate() + "kbps";
|
||||||
|
} else {
|
||||||
|
qualityString = stream.getFormat().getSuffix();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (streamsWrapper.getSizeInBytes(position) > 0) {
|
||||||
|
sizeView.setText(streamsWrapper.getFormattedSize(position));
|
||||||
|
sizeView.setVisibility(View.VISIBLE);
|
||||||
|
} else {
|
||||||
|
sizeView.setVisibility(View.GONE);
|
||||||
|
}
|
||||||
|
|
||||||
|
formatNameView.setText(stream.getFormat().getName());
|
||||||
|
qualityView.setText(qualityString);
|
||||||
|
woSoundIconView.setVisibility(woSoundIconVisibility);
|
||||||
|
|
||||||
|
return convertView;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A wrapper class that includes a way of storing the stream sizes.
|
||||||
|
*/
|
||||||
|
public static class StreamSizeWrapper<T extends Stream> implements Serializable {
|
||||||
|
private static final StreamSizeWrapper<Stream> EMPTY = new StreamSizeWrapper<>(Collections.emptyList());
|
||||||
|
private final List<T> streamsList;
|
||||||
|
private long[] streamSizes;
|
||||||
|
|
||||||
|
public StreamSizeWrapper(List<T> streamsList) {
|
||||||
|
this.streamsList = streamsList;
|
||||||
|
this.streamSizes = new long[streamsList.size()];
|
||||||
|
|
||||||
|
for (int i = 0; i < streamSizes.length; i++) streamSizes[i] = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper method to fetch the sizes of all the streams in a wrapper.
|
||||||
|
*
|
||||||
|
* @param streamsWrapper the wrapper
|
||||||
|
* @return a {@link Single} that returns a boolean indicating if any elements were changed
|
||||||
|
*/
|
||||||
|
public static <X extends Stream> Single<Boolean> fetchSizeForWrapper(StreamSizeWrapper<X> streamsWrapper) {
|
||||||
|
final Callable<Boolean> fetchAndSet = () -> {
|
||||||
|
boolean hasChanged = false;
|
||||||
|
for (X stream : streamsWrapper.getStreamsList()) {
|
||||||
|
if (streamsWrapper.getSizeInBytes(stream) > 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
final long contentLength = Downloader.getInstance().getContentLength(stream.getUrl());
|
||||||
|
streamsWrapper.setSize(stream, contentLength);
|
||||||
|
hasChanged = true;
|
||||||
|
}
|
||||||
|
return hasChanged;
|
||||||
|
};
|
||||||
|
|
||||||
|
return Single.fromCallable(fetchAndSet)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.onErrorReturnItem(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<T> getStreamsList() {
|
||||||
|
return streamsList;
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSizeInBytes(int streamIndex) {
|
||||||
|
return streamSizes[streamIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
public long getSizeInBytes(T stream) {
|
||||||
|
return streamSizes[streamsList.indexOf(stream)];
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormattedSize(int streamIndex) {
|
||||||
|
return Utility.formatBytes(getSizeInBytes(streamIndex));
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getFormattedSize(T stream) {
|
||||||
|
return Utility.formatBytes(getSizeInBytes(stream));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(int streamIndex, long sizeInBytes) {
|
||||||
|
streamSizes[streamIndex] = sizeInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSize(T stream, long sizeInBytes) {
|
||||||
|
streamSizes[streamsList.indexOf(stream)] = sizeInBytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static <X extends Stream> StreamSizeWrapper<X> empty() {
|
||||||
|
//noinspection unchecked
|
||||||
|
return (StreamSizeWrapper<X>) EMPTY;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -35,8 +35,8 @@
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:layout_below="@+id/file_name"
|
android:layout_below="@+id/file_name"
|
||||||
android:layout_marginLeft="20dp"
|
|
||||||
android:layout_marginBottom="6dp"
|
android:layout_marginBottom="6dp"
|
||||||
|
android:layout_marginLeft="20dp"
|
||||||
android:gravity="left"
|
android:gravity="left"
|
||||||
android:orientation="horizontal"
|
android:orientation="horizontal"
|
||||||
tools:ignore="RtlHardcoded">
|
tools:ignore="RtlHardcoded">
|
||||||
|
@ -59,12 +59,12 @@
|
||||||
android:id="@+id/quality_spinner"
|
android:id="@+id/quality_spinner"
|
||||||
android:layout_width="match_parent"
|
android:layout_width="match_parent"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
android:minWidth="150dp"
|
|
||||||
android:layout_below="@+id/video_audio_group"
|
android:layout_below="@+id/video_audio_group"
|
||||||
android:layout_marginBottom="12dp"
|
android:layout_marginBottom="12dp"
|
||||||
android:layout_marginLeft="20dp"
|
android:layout_marginLeft="20dp"
|
||||||
android:layout_marginRight="20dp"
|
android:layout_marginRight="20dp"
|
||||||
tools:listitem="@layout/resolutions_spinner_item"/>
|
android:minWidth="150dp"
|
||||||
|
tools:listitem="@layout/stream_quality_item"/>
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/threads_text_view"
|
android:id="@+id/threads_text_view"
|
||||||
|
@ -82,8 +82,8 @@
|
||||||
android:layout_below="@+id/threads_text_view"
|
android:layout_below="@+id/threads_text_view"
|
||||||
android:layout_marginLeft="24dp"
|
android:layout_marginLeft="24dp"
|
||||||
android:layout_marginRight="24dp"
|
android:layout_marginRight="24dp"
|
||||||
android:paddingBottom="12dp"
|
android:orientation="horizontal"
|
||||||
android:orientation="horizontal">
|
android:paddingBottom="12dp">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/threads_count"
|
android:id="@+id/threads_count"
|
|
@ -1,34 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<RelativeLayout
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="48dp">
|
|
||||||
|
|
||||||
<ImageView
|
|
||||||
android:id="@+id/wo_sound_icon"
|
|
||||||
android:layout_width="20dp"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:layout_alignParentLeft="true"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginLeft="4dp"
|
|
||||||
android:scaleType="fitCenter"
|
|
||||||
android:src="?attr/volume_off"
|
|
||||||
tools:ignore="ContentDescription,RtlHardcoded"/>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@android:id/text1"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_centerVertical="true"
|
|
||||||
android:layout_marginLeft="8dp"
|
|
||||||
android:layout_marginRight="8dp"
|
|
||||||
android:layout_toRightOf="@+id/wo_sound_icon"
|
|
||||||
android:ellipsize="end"
|
|
||||||
android:gravity="left"
|
|
||||||
android:maxLines="1"
|
|
||||||
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
|
||||||
android:textSize="14sp"
|
|
||||||
tools:ignore="RtlHardcoded"
|
|
||||||
tools:text="MPEG-4 1080p60"/>
|
|
||||||
</RelativeLayout>
|
|
66
app/src/main/res/layout/stream_quality_item.xml
Normal file
66
app/src/main/res/layout/stream_quality_item.xml
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<RelativeLayout
|
||||||
|
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="48dp">
|
||||||
|
|
||||||
|
<ImageView
|
||||||
|
android:id="@+id/wo_sound_icon"
|
||||||
|
android:layout_width="20dp"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:layout_alignParentLeft="true"
|
||||||
|
android:layout_centerVertical="true"
|
||||||
|
android:layout_marginLeft="6dp"
|
||||||
|
android:scaleType="fitCenter"
|
||||||
|
android:src="?attr/volume_off"
|
||||||
|
tools:ignore="ContentDescription,RtlHardcoded"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stream_format_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="22dp"
|
||||||
|
android:layout_alignParentTop="true"
|
||||||
|
android:layout_toRightOf="@+id/wo_sound_icon"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="left|bottom"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingLeft="12dp"
|
||||||
|
android:paddingRight="18dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||||
|
android:textSize="12sp"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="MPEG-4"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stream_quality"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="26dp"
|
||||||
|
android:layout_alignParentBottom="true"
|
||||||
|
android:layout_toRightOf="@+id/wo_sound_icon"
|
||||||
|
android:ellipsize="end"
|
||||||
|
android:gravity="left|top"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingLeft="12dp"
|
||||||
|
android:paddingRight="18dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Body1"
|
||||||
|
android:textSize="14sp"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="1080p60"/>
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/stream_size"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="48dp"
|
||||||
|
android:layout_alignParentRight="true"
|
||||||
|
android:gravity="right|center_vertical"
|
||||||
|
android:maxLines="1"
|
||||||
|
android:paddingLeft="8dp"
|
||||||
|
android:paddingRight="8dp"
|
||||||
|
android:textAppearance="@style/TextAppearance.AppCompat.Caption"
|
||||||
|
android:textSize="12sp"
|
||||||
|
android:visibility="gone"
|
||||||
|
tools:ignore="RtlHardcoded"
|
||||||
|
tools:text="123.4 MB"
|
||||||
|
tools:visibility="visible"/>
|
||||||
|
</RelativeLayout>
|
|
@ -1,8 +1,27 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<menu xmlns:android="http://schemas.android.com/apk/res/android">
|
<menu xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto">
|
||||||
|
|
||||||
<item android:id="@+id/action_about"
|
<item
|
||||||
android:orderInCategory="1000"
|
android:id="@+id/action_show_downloads"
|
||||||
android:title="@string/action_about"/>
|
android:orderInCategory="980"
|
||||||
|
android:title="@string/downloads"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_history"
|
||||||
|
android:orderInCategory="981"
|
||||||
|
android:title="@string/action_history"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_settings"
|
||||||
|
android:orderInCategory="990"
|
||||||
|
android:title="@string/settings"
|
||||||
|
app:showAsAction="never"/>
|
||||||
|
|
||||||
|
<item
|
||||||
|
android:id="@+id/action_about"
|
||||||
|
android:orderInCategory="1000"
|
||||||
|
android:title="@string/action_about"/>
|
||||||
</menu>
|
</menu>
|
|
@ -180,6 +180,7 @@
|
||||||
<string name="invalid_file">File doesn\'t exist or insufficient permission to read or write to it</string>
|
<string name="invalid_file">File doesn\'t exist or insufficient permission to read or write to it</string>
|
||||||
<string name="file_name_empty_error">File name cannot be empty</string>
|
<string name="file_name_empty_error">File name cannot be empty</string>
|
||||||
<string name="error_occurred_detail">An error occurred: %1$s</string>
|
<string name="error_occurred_detail">An error occurred: %1$s</string>
|
||||||
|
<string name="no_streams_available_download">No streams available to download</string>
|
||||||
|
|
||||||
<!-- error activity -->
|
<!-- error activity -->
|
||||||
<string name="sorry_string">Sorry, that should not have happened.</string>
|
<string name="sorry_string">Sorry, that should not have happened.</string>
|
||||||
|
|
1
fastlane/metadata/android/de/full_description.txt
Normal file
1
fastlane/metadata/android/de/full_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
NewPipe verwendet keine Bibliotheken des Google Frameworks oder der YouTube API. Es analysiert die Webseite, um die benötigten Informationen zu erlangen. Aus diesem Grund kann die App ohne die Google Services verwendet werden. Ebenso wird kein YouTube-Konto für NewPipe benötigt und es ist FLOSS.
|
1
fastlane/metadata/android/de/short_description.txt
Normal file
1
fastlane/metadata/android/de/short_description.txt
Normal file
|
@ -0,0 +1 @@
|
||||||
|
Eine freie, leichtgewichtige YouTube App für Android
|
Loading…
Reference in a new issue