diff options
author | ByteHamster <ByteHamster@users.noreply.github.com> | 2024-03-29 13:38:31 +0100 |
---|---|---|
committer | GitHub <noreply@github.com> | 2024-03-29 13:38:31 +0100 |
commit | 8f553f08f0347b73b34c80dddef228302cdf5977 (patch) | |
tree | 112ef05988be5a37bb08ec2aa06ed3727234bbae /ui/discovery/src | |
parent | 5ede21d676af8d879ec8f5d695255ccbf4398e76 (diff) | |
download | AntennaPod-8f553f08f0347b73b34c80dddef228302cdf5977.zip |
Add :ui:discovery module (#7037)
Diffstat (limited to 'ui/discovery/src')
12 files changed, 1197 insertions, 0 deletions
diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java new file mode 100644 index 000000000..9cdcdbcb9 --- /dev/null +++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/DiscoveryFragment.java @@ -0,0 +1,276 @@ +package de.danoeh.antennapod.ui.discovery; + +import android.content.Context; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; +import androidx.annotation.NonNull; +import androidx.appcompat.widget.Toolbar; +import androidx.fragment.app.Fragment; +import com.google.android.material.appbar.MaterialToolbar; +import com.google.android.material.dialog.MaterialAlertDialogBuilder; +import com.google.android.material.textfield.MaterialAutoCompleteTextView; +import com.google.android.material.textfield.TextInputLayout; +import de.danoeh.antennapod.net.discovery.BuildConfig; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent; +import de.danoeh.antennapod.net.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; +import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * Searches iTunes store for top podcasts and displays results in a list. + */ +public class DiscoveryFragment extends Fragment implements Toolbar.OnMenuItemClickListener { + public static final String TAG = "DiscoveryFragment"; + private static final int NUM_OF_TOP_PODCASTS = 25; + private SharedPreferences prefs; + + /** + * Adapter responsible with the search results. + */ + private OnlineSearchAdapter adapter; + private GridView gridView; + private ProgressBar progressBar; + private TextView txtvError; + private Button butRetry; + private TextView txtvEmpty; + + /** + * List of podcasts retreived from the search. + */ + private List<PodcastSearchResult> searchResults; + private List<PodcastSearchResult> topList; + private Disposable disposable; + private String countryCode = "US"; + private boolean hidden; + private boolean needsConfirm; + private MaterialToolbar toolbar; + + public DiscoveryFragment() { + // Required empty public constructor + } + + /** + * Replace adapter data with provided search results from SearchTask. + * + * @param result List of Podcast objects containing search results + */ + private void updateData(List<PodcastSearchResult> result) { + this.searchResults = result; + adapter.clear(); + if (result != null && result.size() > 0) { + gridView.setVisibility(View.VISIBLE); + txtvEmpty.setVisibility(View.GONE); + for (PodcastSearchResult p : result) { + adapter.add(p); + } + adapter.notifyDataSetInvalidated(); + } else { + gridView.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.VISIBLE); + } + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + prefs = getActivity().getSharedPreferences(ItunesTopListLoader.PREFS, Context.MODE_PRIVATE); + countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, Locale.getDefault().getCountry()); + hidden = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false); + needsConfirm = prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true); + } + + @Override + public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment + View root = inflater.inflate(R.layout.fragment_online_search, container, false); + gridView = root.findViewById(R.id.gridView); + adapter = new OnlineSearchAdapter(getActivity(), new ArrayList<>()); + gridView.setAdapter(adapter); + + toolbar = root.findViewById(R.id.toolbar); + toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); + toolbar.inflateMenu(R.menu.countries_menu); + MenuItem discoverHideItem = toolbar.getMenu().findItem(R.id.discover_hide_item); + discoverHideItem.setChecked(hidden); + toolbar.setOnMenuItemClickListener(this); + + //Show information about the podcast when the list item is clicked + gridView.setOnItemClickListener((parent, view1, position, id) -> { + PodcastSearchResult podcast = searchResults.get(position); + if (podcast.feedUrl == null) { + return; + } + startActivity(new OnlineFeedviewActivityStarter(getContext(), podcast.feedUrl).getIntent()); + }); + + progressBar = root.findViewById(R.id.progressBar); + txtvError = root.findViewById(R.id.txtvError); + butRetry = root.findViewById(R.id.butRetry); + txtvEmpty = root.findViewById(android.R.id.empty); + + loadToplist(countryCode); + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + adapter = null; + } + + private void loadToplist(String country) { + if (disposable != null) { + disposable.dispose(); + } + + gridView.setVisibility(View.GONE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + butRetry.setText(R.string.retry_label); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + + if (hidden) { + gridView.setVisibility(View.GONE); + txtvError.setVisibility(View.VISIBLE); + txtvError.setText(getResources().getString(R.string.discover_is_hidden)); + butRetry.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + return; + } + //noinspection ConstantConditions + if (BuildConfig.FLAVOR.equals("free") && needsConfirm) { + txtvError.setVisibility(View.VISIBLE); + txtvError.setText(""); + butRetry.setVisibility(View.VISIBLE); + butRetry.setText(R.string.discover_confirm); + butRetry.setOnClickListener(v -> { + prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply(); + needsConfirm = false; + loadToplist(country); + }); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + return; + } + + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + disposable = Observable.fromCallable(() -> + loader.loadToplist(country, NUM_OF_TOP_PODCASTS, DBReader.getFeedList())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + podcasts -> { + progressBar.setVisibility(View.GONE); + topList = podcasts; + updateData(topList); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> loadToplist(country)); + butRetry.setVisibility(View.VISIBLE); + }); + } + + @Override + public boolean onMenuItemClick(MenuItem item) { + final int itemId = item.getItemId(); + if (itemId == R.id.discover_hide_item) { + item.setChecked(!item.isChecked()); + hidden = item.isChecked(); + prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply(); + + EventBus.getDefault().post(new DiscoveryDefaultUpdateEvent()); + loadToplist(countryCode); + return true; + } else if (itemId == R.id.discover_countries_item) { + + LayoutInflater inflater = getLayoutInflater(); + View selectCountryDialogView = inflater.inflate(R.layout.select_country_dialog, null); + MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext()); + builder.setView(selectCountryDialogView); + + List<String> countryCodeArray = new ArrayList<>(Arrays.asList(Locale.getISOCountries())); + Map<String, String> countryCodeNames = new HashMap<>(); + Map<String, String> countryNameCodes = new HashMap<>(); + for (String code : countryCodeArray) { + Locale locale = new Locale("", code); + String countryName = locale.getDisplayCountry(); + countryCodeNames.put(code, countryName); + countryNameCodes.put(countryName, code); + } + + List<String> countryNamesSort = new ArrayList<>(countryCodeNames.values()); + Collections.sort(countryNamesSort); + + ArrayAdapter<String> dataAdapter = + new ArrayAdapter<>(this.getContext(), android.R.layout.simple_list_item_1, countryNamesSort); + TextInputLayout textInput = selectCountryDialogView.findViewById(R.id.country_text_input); + MaterialAutoCompleteTextView editText = (MaterialAutoCompleteTextView) textInput.getEditText(); + editText.setAdapter(dataAdapter); + editText.setText(countryCodeNames.get(countryCode)); + editText.setOnClickListener(view -> { + if (editText.getText().length() != 0) { + editText.setText(""); + editText.postDelayed(editText::showDropDown, 100); + } + }); + editText.setOnFocusChangeListener((v, hasFocus) -> { + if (hasFocus) { + editText.setText(""); + editText.postDelayed(editText::showDropDown, 100); + } + }); + + builder.setPositiveButton(android.R.string.ok, (dialogInterface, i) -> { + String countryName = editText.getText().toString(); + if (countryNameCodes.containsKey(countryName)) { + countryCode = countryNameCodes.get(countryName); + MenuItem discoverHideItem = toolbar.getMenu().findItem(R.id.discover_hide_item); + discoverHideItem.setChecked(false); + hidden = false; + } + + prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, hidden).apply(); + prefs.edit().putString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, countryCode).apply(); + + EventBus.getDefault().post(new DiscoveryDefaultUpdateEvent()); + loadToplist(countryCode); + }); + builder.setNegativeButton(R.string.cancel_label, null); + builder.show(); + return true; + } + return false; + } +} diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java new file mode 100644 index 000000000..ac400da9f --- /dev/null +++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/FeedDiscoverAdapter.java @@ -0,0 +1,79 @@ +package de.danoeh.antennapod.ui.discovery; + +import android.content.Context; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ImageView; +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; + +import java.util.ArrayList; +import java.util.List; + +public class FeedDiscoverAdapter extends BaseAdapter { + + private final List<PodcastSearchResult> data = new ArrayList<>(); + private final Context context; + + public FeedDiscoverAdapter(Context context) { + this.context = context; + } + + public void updateData(List<PodcastSearchResult> newData) { + data.clear(); + data.addAll(newData); + notifyDataSetChanged(); + } + + @Override + public int getCount() { + return data.size(); + } + + @Override + public PodcastSearchResult getItem(int position) { + return data.get(position); + } + + @Override + public long getItemId(int position) { + return 0; + } + + @Override + public View getView(int position, View convertView, ViewGroup parent) { + Holder holder; + + if (convertView == null) { + convertView = View.inflate(context, R.layout.quick_feed_discovery_item, null); + holder = new Holder(); + holder.imageView = convertView.findViewById(R.id.discovery_cover); + convertView.setTag(holder); + } else { + holder = (Holder) convertView.getTag(); + } + + + final PodcastSearchResult podcast = getItem(position); + holder.imageView.setContentDescription(podcast.title); + + Glide.with(context) + .load(podcast.imageUrl) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .transform(new FitCenter(), new RoundedCorners((int) + (8 * context.getResources().getDisplayMetrics().density))) + .dontAnimate()) + .into(holder.imageView); + + return convertView; + } + + static class Holder { + ImageView imageView; + } +} diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java new file mode 100644 index 000000000..a536b8ebd --- /dev/null +++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchAdapter.java @@ -0,0 +1,122 @@ +package de.danoeh.antennapod.ui.discovery; + +import android.content.Context; +import android.widget.ArrayAdapter; +import androidx.annotation.NonNull; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import com.bumptech.glide.Glide; +import com.bumptech.glide.load.engine.DiskCacheStrategy; + +import com.bumptech.glide.load.resource.bitmap.FitCenter; +import com.bumptech.glide.load.resource.bitmap.RoundedCorners; +import com.bumptech.glide.request.RequestOptions; + +import java.util.List; + +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; + +public class OnlineSearchAdapter extends ArrayAdapter<PodcastSearchResult> { + /** + * Related Context + */ + private final Context context; + + /** + * List holding the podcasts found in the search + */ + private final List<PodcastSearchResult> data; + + /** + * Constructor. + * + * @param context Related context + * @param objects Search result + */ + public OnlineSearchAdapter(Context context, List<PodcastSearchResult> objects) { + super(context, 0, objects); + this.data = objects; + this.context = context; + } + + @NonNull + @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { + //Current podcast + PodcastSearchResult podcast = data.get(position); + + //ViewHolder + PodcastViewHolder viewHolder; + + //Resulting view + View view; + + //Handle view holder stuff + if (convertView == null) { + view = View.inflate(context, R.layout.online_search_listitem, null); + viewHolder = new PodcastViewHolder(view); + view.setTag(viewHolder); + } else { + view = convertView; + viewHolder = (PodcastViewHolder) view.getTag(); + } + + // Set the title + viewHolder.titleView.setText(podcast.title); + if (podcast.author != null && ! podcast.author.trim().isEmpty()) { + viewHolder.authorView.setText(podcast.author); + viewHolder.authorView.setVisibility(View.VISIBLE); + } else if (podcast.feedUrl != null && !podcast.feedUrl.contains("itunes.apple.com")) { + viewHolder.authorView.setText(podcast.feedUrl); + viewHolder.authorView.setVisibility(View.VISIBLE); + } else { + viewHolder.authorView.setVisibility(View.GONE); + } + + //Update the empty imageView with the image from the feed + Glide.with(context) + .load(podcast.imageUrl) + .apply(new RequestOptions() + .placeholder(R.color.light_gray) + .diskCacheStrategy(DiskCacheStrategy.NONE) + .transform(new FitCenter(), + new RoundedCorners((int) (4 * context.getResources().getDisplayMetrics().density))) + .dontAnimate()) + .into(viewHolder.coverView); + + //Feed the grid view + return view; + } + + /** + * View holder object for the GridView + */ + static class PodcastViewHolder { + + /** + * ImageView holding the Podcast image + */ + final ImageView coverView; + + /** + * TextView holding the Podcast title + */ + final TextView titleView; + + final TextView authorView; + + + /** + * Constructor + * @param view GridView cell + */ + PodcastViewHolder(View view) { + coverView = view.findViewById(R.id.imgvCover); + titleView = view.findViewById(R.id.txtvTitle); + authorView = view.findViewById(R.id.txtvAuthor); + } + } +} diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java new file mode 100644 index 000000000..00459a174 --- /dev/null +++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/OnlineSearchFragment.java @@ -0,0 +1,219 @@ +package de.danoeh.antennapod.ui.discovery; + +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; +import android.widget.AbsListView; +import android.widget.Button; +import android.widget.GridView; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.appcompat.widget.SearchView; +import androidx.fragment.app.Fragment; + +import com.google.android.material.appbar.MaterialToolbar; + +import java.util.ArrayList; +import java.util.List; + +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; +import de.danoeh.antennapod.net.discovery.PodcastSearcher; +import de.danoeh.antennapod.net.discovery.PodcastSearcherRegistry; +import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; +import io.reactivex.disposables.Disposable; + +public class OnlineSearchFragment extends Fragment { + + private static final String TAG = "FyydSearchFragment"; + private static final String ARG_SEARCHER = "searcher"; + private static final String ARG_QUERY = "query"; + + /** + * Adapter responsible with the search results + */ + private OnlineSearchAdapter adapter; + private PodcastSearcher searchProvider; + private GridView gridView; + private ProgressBar progressBar; + private TextView txtvError; + private Button butRetry; + private TextView txtvEmpty; + + /** + * List of podcasts retreived from the search + */ + private List<PodcastSearchResult> searchResults; + private Disposable disposable; + + public static OnlineSearchFragment newInstance(Class<? extends PodcastSearcher> searchProvider) { + return newInstance(searchProvider, null); + } + + public static OnlineSearchFragment newInstance(Class<? extends PodcastSearcher> searchProvider, String query) { + OnlineSearchFragment fragment = new OnlineSearchFragment(); + Bundle arguments = new Bundle(); + arguments.putString(ARG_SEARCHER, searchProvider.getName()); + arguments.putString(ARG_QUERY, query); + fragment.setArguments(arguments); + return fragment; + } + + /** + * Constructor + */ + public OnlineSearchFragment() { + // Required empty public constructor + } + + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + for (PodcastSearcherRegistry.SearcherInfo info : PodcastSearcherRegistry.getSearchProviders()) { + if (info.searcher.getClass().getName().equals(getArguments().getString(ARG_SEARCHER))) { + searchProvider = info.searcher; + break; + } + } + if (searchProvider == null) { + throw new IllegalArgumentException("Podcast searcher not found"); + } + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + // Inflate the layout for this fragment + View root = inflater.inflate(R.layout.fragment_online_search, container, false); + gridView = root.findViewById(R.id.gridView); + adapter = new OnlineSearchAdapter(getActivity(), new ArrayList<>()); + gridView.setAdapter(adapter); + + //Show information about the podcast when the list item is clicked + gridView.setOnItemClickListener((parent, view1, position, id) -> { + PodcastSearchResult podcast = searchResults.get(position); + startActivity(new OnlineFeedviewActivityStarter(getContext(), podcast.feedUrl) + .withStartedFromSearch().getIntent()); + }); + progressBar = root.findViewById(R.id.progressBar); + txtvError = root.findViewById(R.id.txtvError); + butRetry = root.findViewById(R.id.butRetry); + txtvEmpty = root.findViewById(android.R.id.empty); + TextView txtvPoweredBy = root.findViewById(R.id.search_powered_by); + txtvPoweredBy.setText(getString(R.string.search_powered_by, searchProvider.getName())); + setupToolbar(root.findViewById(R.id.toolbar)); + + gridView.setOnScrollListener(new AbsListView.OnScrollListener() { + @Override + public void onScrollStateChanged(AbsListView view, int scrollState) { + if (scrollState == SCROLL_STATE_TOUCH_SCROLL) { + InputMethodManager imm = (InputMethodManager) + getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + } + + @Override + public void onScroll(AbsListView view, int firstVisibleItem, int visibleItemCount, int totalItemCount) { + } + }); + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + if (disposable != null) { + disposable.dispose(); + } + adapter = null; + } + + private void setupToolbar(MaterialToolbar toolbar) { + toolbar.inflateMenu(R.menu.online_search); + toolbar.setNavigationOnClickListener(v -> getParentFragmentManager().popBackStack()); + + MenuItem searchItem = toolbar.getMenu().findItem(R.id.action_search); + final SearchView sv = (SearchView) searchItem.getActionView(); + sv.setQueryHint(getString(R.string.search_podcast_hint)); + sv.setOnQueryTextListener(new SearchView.OnQueryTextListener() { + @Override + public boolean onQueryTextSubmit(String s) { + sv.clearFocus(); + search(s); + return true; + } + + @Override + public boolean onQueryTextChange(String s) { + return false; + } + }); + sv.setOnQueryTextFocusChangeListener((view, hasFocus) -> { + if (hasFocus) { + showInputMethod(view.findFocus()); + } + }); + searchItem.setOnActionExpandListener(new MenuItem.OnActionExpandListener() { + @Override + public boolean onMenuItemActionExpand(MenuItem item) { + return true; + } + + @Override + public boolean onMenuItemActionCollapse(MenuItem item) { + getActivity().getSupportFragmentManager().popBackStack(); + return true; + } + }); + searchItem.expandActionView(); + + if (getArguments().getString(ARG_QUERY, null) != null) { + sv.setQuery(getArguments().getString(ARG_QUERY, null), true); + } + } + + private void search(String query) { + if (disposable != null) { + disposable.dispose(); + } + showOnlyProgressBar(); + disposable = searchProvider.search(query).subscribe(result -> { + searchResults = result; + progressBar.setVisibility(View.GONE); + adapter.clear(); + adapter.addAll(searchResults); + adapter.notifyDataSetInvalidated(); + gridView.setVisibility(!searchResults.isEmpty() ? View.VISIBLE : View.GONE); + txtvEmpty.setVisibility(searchResults.isEmpty() ? View.VISIBLE : View.GONE); + txtvEmpty.setText(getString(R.string.no_results_for_query, query)); + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + progressBar.setVisibility(View.GONE); + txtvError.setText(error.toString()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setOnClickListener(v -> search(query)); + butRetry.setVisibility(View.VISIBLE); + }); + } + + private void showOnlyProgressBar() { + gridView.setVisibility(View.GONE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + txtvEmpty.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + } + + private void showInputMethod(View view) { + InputMethodManager imm = (InputMethodManager) getActivity().getSystemService(Context.INPUT_METHOD_SERVICE); + if (imm != null) { + imm.showSoftInput(view, 0); + } + } +} diff --git a/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java new file mode 100644 index 000000000..ff5e328f2 --- /dev/null +++ b/ui/discovery/src/main/java/de/danoeh/antennapod/ui/discovery/QuickFeedDiscoveryFragment.java @@ -0,0 +1,175 @@ +package de.danoeh.antennapod.ui.discovery; + +import android.content.SharedPreferences; +import android.os.Bundle; +import android.text.TextUtils; +import android.util.DisplayMetrics; + +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.AdapterView; +import android.widget.Button; +import android.widget.GridView; +import android.widget.LinearLayout; +import android.widget.TextView; +import androidx.fragment.app.Fragment; +import de.danoeh.antennapod.net.discovery.BuildConfig; +import de.danoeh.antennapod.storage.database.DBReader; +import de.danoeh.antennapod.event.DiscoveryDefaultUpdateEvent; +import de.danoeh.antennapod.net.discovery.ItunesTopListLoader; +import de.danoeh.antennapod.net.discovery.PodcastSearchResult; +import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter; +import de.danoeh.antennapod.ui.appstartintent.OnlineFeedviewActivityStarter; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import org.greenrobot.eventbus.EventBus; +import org.greenrobot.eventbus.Subscribe; +import org.greenrobot.eventbus.ThreadMode; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import static android.content.Context.MODE_PRIVATE; + + +public class QuickFeedDiscoveryFragment extends Fragment implements AdapterView.OnItemClickListener { + private static final String TAG = "FeedDiscoveryFragment"; + private static final int NUM_SUGGESTIONS = 12; + + private Disposable disposable; + private FeedDiscoverAdapter adapter; + private GridView discoverGridLayout; + private TextView errorTextView; + private TextView poweredByTextView; + private LinearLayout errorView; + private Button errorRetry; + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + super.onCreateView(inflater, container, savedInstanceState); + View root = inflater.inflate(R.layout.quick_feed_discovery, container, false); + View discoverMore = root.findViewById(R.id.discover_more); + discoverMore.setOnClickListener(v -> startActivity(new MainActivityStarter(getContext()) + .withFragmentLoaded(DiscoveryFragment.TAG) + .withAddToBackStack() + .getIntent())); + + discoverGridLayout = root.findViewById(R.id.discover_grid); + errorView = root.findViewById(R.id.discover_error); + errorTextView = root.findViewById(R.id.discover_error_txtV); + errorRetry = root.findViewById(R.id.discover_error_retry_btn); + poweredByTextView = root.findViewById(R.id.discover_powered_by_itunes); + + adapter = new FeedDiscoverAdapter(getActivity()); + discoverGridLayout.setAdapter(adapter); + discoverGridLayout.setOnItemClickListener(this); + + DisplayMetrics displayMetrics = getContext().getResources().getDisplayMetrics(); + float screenWidthDp = displayMetrics.widthPixels / displayMetrics.density; + if (screenWidthDp > 600) { + discoverGridLayout.setNumColumns(6); + } else { + discoverGridLayout.setNumColumns(4); + } + + // Fill with dummy elements to have a fixed height and + // prevent the UI elements below from jumping on slow connections + List<PodcastSearchResult> dummies = new ArrayList<>(); + for (int i = 0; i < NUM_SUGGESTIONS; i++) { + dummies.add(PodcastSearchResult.dummy()); + } + + adapter.updateData(dummies); + loadToplist(); + + EventBus.getDefault().register(this); + return root; + } + + @Override + public void onDestroy() { + super.onDestroy(); + EventBus.getDefault().unregister(this); + if (disposable != null) { + disposable.dispose(); + } + } + + @Subscribe(threadMode = ThreadMode.MAIN) + @SuppressWarnings("unused") + public void onDiscoveryDefaultUpdateEvent(DiscoveryDefaultUpdateEvent event) { + loadToplist(); + } + + private void loadToplist() { + errorView.setVisibility(View.GONE); + errorRetry.setVisibility(View.INVISIBLE); + errorRetry.setText(R.string.retry_label); + poweredByTextView.setVisibility(View.VISIBLE); + + ItunesTopListLoader loader = new ItunesTopListLoader(getContext()); + SharedPreferences prefs = getActivity().getSharedPreferences(ItunesTopListLoader.PREFS, MODE_PRIVATE); + String countryCode = prefs.getString(ItunesTopListLoader.PREF_KEY_COUNTRY_CODE, + Locale.getDefault().getCountry()); + if (prefs.getBoolean(ItunesTopListLoader.PREF_KEY_HIDDEN_DISCOVERY_COUNTRY, false)) { + errorTextView.setText(R.string.discover_is_hidden); + errorView.setVisibility(View.VISIBLE); + discoverGridLayout.setVisibility(View.GONE); + errorRetry.setVisibility(View.GONE); + poweredByTextView.setVisibility(View.GONE); + return; + } + //noinspection ConstantConditions + if (BuildConfig.FLAVOR.equals("free") && prefs.getBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, true)) { + errorTextView.setText(""); + errorView.setVisibility(View.VISIBLE); + discoverGridLayout.setVisibility(View.VISIBLE); + errorRetry.setVisibility(View.VISIBLE); + errorRetry.setText(R.string.discover_confirm); + poweredByTextView.setVisibility(View.VISIBLE); + errorRetry.setOnClickListener(v -> { + prefs.edit().putBoolean(ItunesTopListLoader.PREF_KEY_NEEDS_CONFIRM, false).apply(); + loadToplist(); + }); + return; + } + + disposable = Observable.fromCallable(() -> + loader.loadToplist(countryCode, NUM_SUGGESTIONS, DBReader.getFeedList())) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + podcasts -> { + errorView.setVisibility(View.GONE); + if (podcasts.size() == 0) { + errorTextView.setText(getResources().getText(R.string.search_status_no_results)); + errorView.setVisibility(View.VISIBLE); + discoverGridLayout.setVisibility(View.INVISIBLE); + } else { + discoverGridLayout.setVisibility(View.VISIBLE); + adapter.updateData(podcasts); + } + }, error -> { + Log.e(TAG, Log.getStackTraceString(error)); + errorTextView.setText(error.getLocalizedMessage()); + errorView.setVisibility(View.VISIBLE); + discoverGridLayout.setVisibility(View.INVISIBLE); + errorRetry.setVisibility(View.VISIBLE); + errorRetry.setOnClickListener(v -> loadToplist()); + }); + } + + @Override + public void onItemClick(AdapterView<?> parent, final View view, int position, long id) { + PodcastSearchResult podcast = adapter.getItem(position); + if (TextUtils.isEmpty(podcast.feedUrl)) { + return; + } + startActivity(new OnlineFeedviewActivityStarter(getContext(), podcast.feedUrl).getIntent()); + } +} diff --git a/ui/discovery/src/main/res/layout/fragment_online_search.xml b/ui/discovery/src/main/res/layout/fragment_online_search.xml new file mode 100644 index 000000000..902dd8b74 --- /dev/null +++ b/ui/discovery/src/main/res/layout/fragment_online_search.xml @@ -0,0 +1,101 @@ +<?xml version="1.0" encoding="utf-8"?> +<RelativeLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <com.google.android.material.appbar.AppBarLayout + android:id="@+id/appbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_alignParentTop="true" + android:fitsSystemWindows="true" + android:elevation="0dp"> + + <com.google.android.material.appbar.MaterialToolbar + android:id="@+id/toolbar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:minHeight="?attr/actionBarSize" + android:theme="?attr/actionBarTheme" + app:title="@string/discover" + app:navigationContentDescription="@string/toolbar_back_button_content_description" + app:navigationIcon="?homeAsUpIndicator" /> + + </com.google.android.material.appbar.AppBarLayout> + + <GridView + android:id="@+id/gridView" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_below="@id/appbar" + android:clipToPadding="false" + android:columnWidth="400dp" + android:gravity="center" + android:horizontalSpacing="8dp" + android:numColumns="auto_fit" + android:paddingBottom="@dimen/list_vertical_padding" + android:paddingTop="@dimen/list_vertical_padding" + android:stretchMode="columnWidth" + android:verticalSpacing="8dp" + tools:listitem="@layout/online_search_listitem" /> + + <TextView + android:id="@android:id/empty" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_centerInParent="true" + android:gravity="center" + android:visibility="gone" + android:text="@string/search_status_no_results" /> + + <ProgressBar + android:id="@+id/progressBar" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:indeterminateOnly="true" + android:visibility="gone" /> + + <TextView + android:id="@+id/txtvError" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:layout_margin="16dp" + android:textAlignment="center" + android:textSize="@dimen/text_size_small" + android:visibility="gone" + tools:visibility="visible" + tools:text="Error message" + tools:background="@android:color/holo_red_light" /> + + <Button + android:id="@+id/butRetry" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_below="@id/txtvError" + android:layout_centerHorizontal="true" + android:layout_margin="16dp" + android:text="@string/retry_label" + android:visibility="gone" + tools:visibility="visible" + tools:background="@android:color/holo_red_light" /> + + <TextView + android:id="@+id/search_powered_by" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorTertiary" + android:text="@string/discover_powered_by_itunes" + android:textSize="12sp" + android:padding="4dp" + android:background="?android:attr/colorBackground" + android:layout_alignParentBottom="true" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:textAlignment="textEnd" /> + +</RelativeLayout> diff --git a/ui/discovery/src/main/res/layout/online_search_listitem.xml b/ui/discovery/src/main/res/layout/online_search_listitem.xml new file mode 100644 index 000000000..da2de457b --- /dev/null +++ b/ui/discovery/src/main/res/layout/online_search_listitem.xml @@ -0,0 +1,59 @@ +<?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="wrap_content" + android:paddingTop="8dp" + android:paddingLeft="16dp" + android:paddingRight="16dp" + android:paddingBottom="8dp" + tools:background="@android:color/darker_gray"> + + <ImageView + android:id="@+id/imgvCover" + android:layout_width="@dimen/thumbnail_length_itemlist" + android:layout_height="@dimen/thumbnail_length_itemlist" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:adjustViewBounds="true" + android:importantForAccessibility="no" + android:cropToPadding="true" + android:scaleType="fitXY" + tools:background="@android:color/holo_green_dark" + tools:src="@tools:sample/avatars" /> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_toRightOf="@id/imgvCover" + android:layout_toEndOf="@id/imgvCover" + android:layout_centerVertical="true" + android:orientation="vertical" + android:layout_marginLeft="16dp" + android:layout_marginStart="16dp"> + + <TextView + android:id="@+id/txtvTitle" + android:layout_width="match_parent" + android:layout_height="wrap_content" + style="@style/AntennaPod.TextView.ListItemPrimaryTitle" + tools:background="@android:color/holo_green_dark" + tools:text="Podcast title" /> + + <TextView + android:id="@+id/txtvAuthor" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textSize="14sp" + android:textColor="?android:attr/textColorSecondary" + android:ellipsize="middle" + android:maxLines="2" + style="android:style/TextAppearance.Small" + tools:text="author" + tools:background="@android:color/holo_green_dark" /> + + </LinearLayout> + +</RelativeLayout> diff --git a/ui/discovery/src/main/res/layout/quick_feed_discovery.xml b/ui/discovery/src/main/res/layout/quick_feed_discovery.xml new file mode 100644 index 000000000..f4e406d20 --- /dev/null +++ b/ui/discovery/src/main/res/layout/quick_feed_discovery.xml @@ -0,0 +1,92 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + + <LinearLayout + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="horizontal"> + + <TextView + android:layout_width="0dip" + android:layout_height="wrap_content" + android:text="@string/discover" + android:textSize="18sp" + android:layout_marginBottom="8dp" + android:layout_weight="1" + android:accessibilityHeading="true" + android:textColor="?android:attr/textColorPrimary" /> + + <Button + android:id="@+id/discover_more" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:minHeight="48dp" + android:minWidth="0dp" + android:text="@string/discover_more" + style="@style/Widget.MaterialComponents.Button.TextButton" /> + + </LinearLayout> + + <RelativeLayout + android:layout_width="match_parent" + android:layout_height="wrap_content"> + + <de.danoeh.antennapod.ui.common.WrappingGridView + android:id="@+id/discover_grid" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:numColumns="4" + android:scrollbars="none" + android:layout_marginTop="8dp" + android:layout_centerInParent="true" + android:layout_gravity="center_horizontal" + android:layout_columnWeight="1" + android:layout_rowWeight="1" /> + + <LinearLayout + android:id="@+id/discover_error" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_centerInParent="true" + android:gravity="center" + android:orientation="vertical"> + + <TextView + android:id="@+id/discover_error_txtV" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center" + android:layout_margin="16dp" + android:textSize="@dimen/text_size_small" + tools:text="Error message" + tools:background="@android:color/holo_red_light" /> + + <Button + android:id="@+id/discover_error_retry_btn" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_margin="16dp" + android:text="@string/retry_label" + tools:background="@android:color/holo_red_light" /> + + </LinearLayout> + + </RelativeLayout> + + <TextView + android:id="@+id/discover_powered_by_itunes" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:textColor="?android:attr/textColorTertiary" + android:text="@string/discover_powered_by_itunes" + android:textSize="12sp" + android:layout_gravity="right|end" + android:paddingHorizontal="4dp" + android:textAlignment="textEnd" /> + +</LinearLayout> diff --git a/ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml b/ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml new file mode 100644 index 000000000..4407eb2f5 --- /dev/null +++ b/ui/discovery/src/main/res/layout/quick_feed_discovery_item.xml @@ -0,0 +1,21 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:squareImageView="http://schemas.android.com/apk/de.danoeh.antennapod" + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:padding="4dp" + android:clipToPadding="false"> + + <de.danoeh.antennapod.ui.common.SquareImageView + android:id="@+id/discovery_cover" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:elevation="4dp" + android:outlineProvider="background" + android:foreground="?android:attr/selectableItemBackground" + squareImageView:direction="width" + tools:src="@tools:sample/avatars" /> + +</LinearLayout> diff --git a/ui/discovery/src/main/res/layout/select_country_dialog.xml b/ui/discovery/src/main/res/layout/select_country_dialog.xml new file mode 100644 index 000000000..70f93bb77 --- /dev/null +++ b/ui/discovery/src/main/res/layout/select_country_dialog.xml @@ -0,0 +1,24 @@ +<?xml version="1.0" encoding="utf-8"?> +<LinearLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> + + <com.google.android.material.textfield.TextInputLayout + android:id="@+id/country_text_input" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="20dp" + android:hint="@string/select_country" + app:endIconMode="none" + style="@style/Widget.MaterialComponents.TextInputLayout.FilledBox.ExposedDropdownMenu"> + + <AutoCompleteTextView + android:layout_width="match_parent" + android:layout_height="wrap_content" /> + + </com.google.android.material.textfield.TextInputLayout> + +</LinearLayout> diff --git a/ui/discovery/src/main/res/menu/countries_menu.xml b/ui/discovery/src/main/res/menu/countries_menu.xml new file mode 100644 index 000000000..e0bd7dafe --- /dev/null +++ b/ui/discovery/src/main/res/menu/countries_menu.xml @@ -0,0 +1,17 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/discover_hide_item" + android:checkable="true" + android:enabled="true" + android:title="@string/discover_hide" + android:visible="true" + app:showAsAction="never" /> + <item + android:id="@+id/discover_countries_item" + android:enabled="true" + android:title="@string/select_country" + android:visible="true" /> +</menu>
\ No newline at end of file diff --git a/ui/discovery/src/main/res/menu/online_search.xml b/ui/discovery/src/main/res/menu/online_search.xml new file mode 100644 index 000000000..374a054fa --- /dev/null +++ b/ui/discovery/src/main/res/menu/online_search.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="utf-8"?> +<menu xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:custom="http://schemas.android.com/apk/res-auto"> + + <item + android:id="@+id/action_search" + android:icon="@drawable/ic_search" + custom:showAsAction="collapseActionView|ifRoom" + custom:actionViewClass="androidx.appcompat.widget.SearchView" + android:title="@string/search_label"/> + +</menu> |