Merge remote-tracking branch 'origin/dev' into dev

This commit is contained in:
Weblate 2018-04-09 00:13:42 +02:00
commit f7f86a2f62
16 changed files with 537 additions and 246 deletions

View file

@ -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.

View file

@ -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)" : "");
MenuItem item = drawerItems.getMenu() final 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);
} }

View file

@ -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
public boolean onMenuItemClick(MenuItem item) {
if (item.getItemId() == R.id.okay) { if (item.getItemId() == R.id.okay) {
downloadSelected(); downloadSelected();
return true; 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); streamsSpinner.setAdapter(audioStreamsAdapter);
items[i] = audioStream.getFormat().getName() + " " + audioStream.getAverageBitrate() + "kbps"; streamsSpinner.setSelection(selectedAudioIndex);
setRadioButtonsState(true);
} }
ArrayAdapter<String> itemAdapter = new ArrayAdapter<>(getContext(), android.R.layout.simple_spinner_dropdown_item, items); private void setupVideoSpinner() {
spinner.setAdapter(itemAdapter); if (getContext() == null) return;
spinner.setSelection(selectedAudioIndex);
}
public void setupVideoSpinner(final List<VideoStream> videoStreams, Spinner spinner) { streamsSpinner.setAdapter(videoStreamsAdapter);
spinner.setAdapter(new SpinnerToolbarAdapter(getContext(), videoStreams, true)); streamsSpinner.setSelection(selectedVideoIndex);
spinner.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();
} }

View file

@ -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;
}
}

View file

@ -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
//////////////////////////////////////////////////////////////////////////*/ //////////////////////////////////////////////////////////////////////////*/

View file

@ -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);

View file

@ -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

View 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;
}
}
}

View file

@ -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"

View file

@ -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>

View 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>

View file

@ -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:id="@+id/action_show_downloads"
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:orderInCategory="1000"
android:title="@string/action_about"/> android:title="@string/action_about"/>
</menu> </menu>

View file

@ -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>

View 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.

View file

@ -0,0 +1 @@
Eine freie, leichtgewichtige YouTube App für Android