mirror of
https://github.com/f-droid/fdroidclient.git
synced 2026-04-19 22:37:09 -04:00
Merge branch 'default-categories' into 'master'
Ensure existence of default categories See merge request fdroid/fdroidclient!1241
This commit is contained in:
@@ -1,6 +1,5 @@
|
||||
package org.fdroid.fdroid.views.categories;
|
||||
|
||||
import android.util.Log;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
@@ -14,26 +13,24 @@ import org.fdroid.database.Category;
|
||||
import org.fdroid.database.FDroidDatabase;
|
||||
import org.fdroid.fdroid.R;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
|
||||
public class CategoryAdapter extends ListAdapter<Category, CategoryController> {
|
||||
public class CategoryAdapter extends ListAdapter<CategoryItem, CategoryController> {
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final FDroidDatabase db;
|
||||
private final HashMap<Category, LiveData<List<AppOverviewItem>>> liveData = new HashMap<>();
|
||||
|
||||
public CategoryAdapter(AppCompatActivity activity, FDroidDatabase db) {
|
||||
super(new DiffUtil.ItemCallback<Category>() {
|
||||
super(new DiffUtil.ItemCallback<CategoryItem>() {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull Category oldItem, @NonNull Category newItem) {
|
||||
return oldItem.equals(newItem);
|
||||
public boolean areItemsTheSame(@NonNull CategoryItem oldItem, @NonNull CategoryItem newItem) {
|
||||
return oldItem.category.equals(newItem.category);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Category oldItem, @NonNull Category newItem) {
|
||||
public boolean areContentsTheSame(@NonNull CategoryItem oldItem, @NonNull CategoryItem newItem) {
|
||||
return false;
|
||||
}
|
||||
});
|
||||
@@ -51,31 +48,17 @@ public class CategoryAdapter extends ListAdapter<Category, CategoryController> {
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull CategoryController holder, int position) {
|
||||
Category category = getItem(position);
|
||||
holder.bindModel(category, liveData.get(category), this::onNoApps);
|
||||
CategoryItem item = getItem(position);
|
||||
holder.bindModel(item, liveData.get(item.category));
|
||||
}
|
||||
|
||||
public void setCategories(@NonNull List<Category> categories) {
|
||||
submitList(categories);
|
||||
for (Category category : categories) {
|
||||
public void setCategories(@NonNull List<CategoryItem> items) {
|
||||
submitList(items);
|
||||
for (CategoryItem item : items) {
|
||||
int num = CategoryController.NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW;
|
||||
// we are getting the LiveData here and not in the ViewHolder, so the data gets cached here
|
||||
// this prevents reloads when scrolling
|
||||
liveData.put(category, db.getAppDao().getAppOverviewItems(category.getId(), num));
|
||||
liveData.put(item.category, db.getAppDao().getAppOverviewItems(item.category.getId(), num));
|
||||
}
|
||||
}
|
||||
|
||||
private void onNoApps(Category category) {
|
||||
ArrayList<Category> categories = new ArrayList<>(getCurrentList());
|
||||
Iterator<Category> itr = categories.iterator();
|
||||
while (itr.hasNext()) {
|
||||
Category c = itr.next();
|
||||
if (c.getId().equals(category.getId())) {
|
||||
Log.d("CategoryAdapter", "Removing " + category.getId() + " without apps.");
|
||||
itr.remove();
|
||||
break;
|
||||
}
|
||||
}
|
||||
submitList(categories);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,9 @@ import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.core.util.Consumer;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
@@ -27,12 +25,10 @@ import com.bumptech.glide.Glide;
|
||||
|
||||
import org.fdroid.database.AppOverviewItem;
|
||||
import org.fdroid.database.Category;
|
||||
import org.fdroid.database.FDroidDatabase;
|
||||
import org.fdroid.database.Repository;
|
||||
import org.fdroid.fdroid.FDroidApp;
|
||||
import org.fdroid.fdroid.R;
|
||||
import org.fdroid.fdroid.Utils;
|
||||
import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||
import org.fdroid.fdroid.views.apps.FeatureImage;
|
||||
import org.fdroid.index.v2.FileV2;
|
||||
@@ -41,11 +37,6 @@ import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Random;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
public class CategoryController extends RecyclerView.ViewHolder {
|
||||
|
||||
private static final String TAG = "CategoryController";
|
||||
@@ -57,18 +48,14 @@ public class CategoryController extends RecyclerView.ViewHolder {
|
||||
private final FrameLayout background;
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
private final FDroidDatabase db;
|
||||
static final int NUM_OF_APPS_PER_CATEGORY_ON_OVERVIEW = 20;
|
||||
|
||||
private Category currentCategory;
|
||||
@Nullable
|
||||
private Disposable disposable;
|
||||
|
||||
CategoryController(final AppCompatActivity activity, View itemView) {
|
||||
super(itemView);
|
||||
|
||||
this.activity = activity;
|
||||
db = DBHelper.getDb(activity);
|
||||
|
||||
appCardsAdapter = new AppPreviewAdapter(activity);
|
||||
|
||||
@@ -89,36 +76,32 @@ public class CategoryController extends RecyclerView.ViewHolder {
|
||||
return categoryNameId == 0 ? categoryName : context.getString(categoryNameId);
|
||||
}
|
||||
|
||||
void bindModel(@NonNull Category category, LiveData<List<AppOverviewItem>> liveData,
|
||||
Consumer<Category> onNoApps) {
|
||||
void bindModel(@NonNull CategoryItem item, LiveData<List<AppOverviewItem>> liveData) {
|
||||
loadAppItems(liveData);
|
||||
currentCategory = category;
|
||||
currentCategory = item.category;
|
||||
|
||||
String categoryName = category.getName(LocaleListCompat.getDefault());
|
||||
if (categoryName == null) categoryName = translateCategory(activity, category.getId());
|
||||
String categoryName = item.category.getName(LocaleListCompat.getDefault());
|
||||
if (categoryName == null) categoryName = translateCategory(activity, item.category.getId());
|
||||
heading.setText(categoryName);
|
||||
heading.setContentDescription(activity.getString(R.string.tts_category_name, categoryName));
|
||||
|
||||
viewAll.setVisibility(View.INVISIBLE);
|
||||
loadNumAppsInCategory(onNoApps);
|
||||
|
||||
@ColorInt int backgroundColour = getBackgroundColour(activity, category.getId());
|
||||
@ColorInt int backgroundColour = getBackgroundColour(activity, item.category.getId());
|
||||
background.setBackgroundColor(backgroundColour);
|
||||
|
||||
// try to load image from repo first
|
||||
FileV2 iconFile = category.getIcon(LocaleListCompat.getDefault());
|
||||
Repository repo = FDroidApp.getRepoManager(activity).getRepository(category.getRepoId());
|
||||
FileV2 iconFile = item.category.getIcon(LocaleListCompat.getDefault());
|
||||
Repository repo = FDroidApp.getRepoManager(activity).getRepository(item.category.getRepoId());
|
||||
if (iconFile != null && repo != null) {
|
||||
Log.i(TAG, "Loading remote image for: " + category.getId());
|
||||
Log.i(TAG, "Loading remote image for: " + item.category.getId());
|
||||
Glide.with(activity)
|
||||
.load(Utils.getDownloadRequest(repo, iconFile))
|
||||
.apply(Utils.getAlwaysShowIconRequestOptions())
|
||||
.into(image);
|
||||
} else {
|
||||
// try to get local image resource
|
||||
int categoryImageId = getCategoryResource(activity, category.getId(), "drawable", true);
|
||||
int categoryImageId = getCategoryResource(activity, item.category.getId(), "drawable", true);
|
||||
if (categoryImageId == 0) {
|
||||
Log.w(TAG, "No image for: " + category.getId());
|
||||
Log.w(TAG, "No image for: " + item.category.getId());
|
||||
image.setColour(backgroundColour);
|
||||
image.setImageDrawable(null);
|
||||
Glide.with(activity).clear(image);
|
||||
@@ -127,6 +110,10 @@ public class CategoryController extends RecyclerView.ViewHolder {
|
||||
Glide.with(activity).load(categoryImageId).into(image);
|
||||
}
|
||||
}
|
||||
Resources r = activity.getResources();
|
||||
viewAll.setText(r.getQuantityString(R.plurals.button_view_all_apps_in_category, item.numApps, item.numApps));
|
||||
viewAll.setContentDescription(r.getQuantityString(R.plurals.tts_view_all_in_category, item.numApps,
|
||||
item.numApps, currentCategory));
|
||||
}
|
||||
|
||||
private void loadAppItems(LiveData<List<AppOverviewItem>> liveData) {
|
||||
@@ -141,14 +128,6 @@ public class CategoryController extends RecyclerView.ViewHolder {
|
||||
});
|
||||
}
|
||||
|
||||
private void loadNumAppsInCategory(Consumer<Category> onNoApps) {
|
||||
if (disposable != null) disposable.dispose();
|
||||
disposable = Single.fromCallable(() -> db.getAppDao().getNumberOfAppsInCategory(currentCategory.getId()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(numApps -> setNumAppsInCategory(numApps, onNoApps));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param requiresLowerCaseId Previously categories were translated using strings such as "category_Reading"
|
||||
* for the "Reading" category. Now we also need to have drawable resources such as
|
||||
@@ -181,19 +160,6 @@ public class CategoryController extends RecyclerView.ViewHolder {
|
||||
return Color.HSVToColor(hsv);
|
||||
}
|
||||
|
||||
private void setNumAppsInCategory(int numAppsInCategory, Consumer<Category> onNoApps) {
|
||||
if (numAppsInCategory == 0) {
|
||||
onNoApps.accept(currentCategory);
|
||||
} else {
|
||||
viewAll.setVisibility(View.VISIBLE);
|
||||
Resources r = activity.getResources();
|
||||
viewAll.setText(r.getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory,
|
||||
numAppsInCategory));
|
||||
viewAll.setContentDescription(r.getQuantityString(R.plurals.tts_view_all_in_category, numAppsInCategory,
|
||||
numAppsInCategory, currentCategory));
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressWarnings("FieldCanBeLocal")
|
||||
private final View.OnClickListener onViewAll = new View.OnClickListener() {
|
||||
@Override
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package org.fdroid.fdroid.views.categories;
|
||||
|
||||
import org.fdroid.database.Category;
|
||||
|
||||
public class CategoryItem {
|
||||
|
||||
public final Category category;
|
||||
final int numApps;
|
||||
|
||||
public CategoryItem(Category category, int numApps) {
|
||||
this.category = category;
|
||||
this.numApps = numApps;
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,14 @@
|
||||
package org.fdroid.fdroid.views.main;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.util.ArraySet;
|
||||
import android.util.Log;
|
||||
import android.view.View;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.os.LocaleListCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
@@ -25,11 +29,16 @@ import org.fdroid.fdroid.data.DBHelper;
|
||||
import org.fdroid.fdroid.panic.HidingManager;
|
||||
import org.fdroid.fdroid.views.apps.AppListActivity;
|
||||
import org.fdroid.fdroid.views.categories.CategoryAdapter;
|
||||
import org.fdroid.fdroid.views.categories.CategoryItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import io.reactivex.rxjava3.disposables.Disposable;
|
||||
import io.reactivex.rxjava3.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Responsible for ensuring that the categories view is inflated and then populated correctly.
|
||||
@@ -39,15 +48,18 @@ import java.util.Map;
|
||||
class CategoriesViewBinder implements Observer<List<Category>> {
|
||||
public static final String TAG = "CategoriesViewBinder";
|
||||
|
||||
private final FDroidDatabase db;
|
||||
private final String[] defaultCategories;
|
||||
private final CategoryAdapter categoryAdapter;
|
||||
private final AppCompatActivity activity;
|
||||
private final TextView emptyState;
|
||||
private final RecyclerView categoriesList;
|
||||
@Nullable
|
||||
private Disposable disposable;
|
||||
|
||||
CategoriesViewBinder(final AppCompatActivity activity, FrameLayout parent) {
|
||||
this.activity = activity;
|
||||
FDroidDatabase db = DBHelper.getDb(activity);
|
||||
db = DBHelper.getDb(activity);
|
||||
Transformations.distinctUntilChanged(db.getRepositoryDao().getLiveCategories()).observe(activity, this);
|
||||
defaultCategories = activity.getResources().getStringArray(R.array.defaultCategories);
|
||||
|
||||
View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true);
|
||||
|
||||
@@ -85,27 +97,65 @@ class CategoriesViewBinder implements Observer<List<Category>> {
|
||||
*/
|
||||
@Override
|
||||
public void onChanged(List<Category> categories) {
|
||||
LocaleListCompat localeListCompat = LocaleListCompat.getDefault();
|
||||
Collections.sort(categories, (o1, o2) -> {
|
||||
String name1 = o1.getName(localeListCompat);
|
||||
if (name1 == null) name1 = o1.getId();
|
||||
String name2 = o2.getName(localeListCompat);
|
||||
if (name2 == null) name2 = o2.getId();
|
||||
return name1.compareToIgnoreCase(name2);
|
||||
});
|
||||
// TODO force-adding nightly category here can be removed once fdroidserver LTS supports defining categories
|
||||
ArrayList<Category> c = new ArrayList<>(categories);
|
||||
Map<String, String> name = Collections.singletonMap("en-US", activity.getString(R.string.category_Nightly));
|
||||
c.add(new Category(42L, "nightly", Collections.emptyMap(), name, Collections.emptyMap()));
|
||||
categoryAdapter.setCategories(c);
|
||||
if (disposable != null) disposable.dispose();
|
||||
disposable = Single.fromCallable(() -> loadCategoryItems(categories))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::onItemsLoaded);
|
||||
}
|
||||
|
||||
if (categoryAdapter.getItemCount() == 0) {
|
||||
private void onItemsLoaded(List<CategoryItem> items) {
|
||||
if (items.size() == 0) {
|
||||
emptyState.setVisibility(View.VISIBLE);
|
||||
categoriesList.setVisibility(View.GONE);
|
||||
} else {
|
||||
categoryAdapter.setCategories(items);
|
||||
emptyState.setVisibility(View.GONE);
|
||||
categoriesList.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
private List<CategoryItem> loadCategoryItems(List<Category> categories) {
|
||||
// get items
|
||||
ArrayList<CategoryItem> items = new ArrayList<>();
|
||||
ArraySet<String> ids = new ArraySet<>(categories.size());
|
||||
for (Category c : categories) {
|
||||
int numApps = db.getAppDao().getNumberOfAppsInCategory(c.getId());
|
||||
if (numApps > 0) {
|
||||
ids.add(c.getId());
|
||||
CategoryItem item = new CategoryItem(c, numApps);
|
||||
items.add(item);
|
||||
} else {
|
||||
Log.d(TAG, "Not adding " + c.getId() + " because it has no apps.");
|
||||
}
|
||||
}
|
||||
// add default categories, if they are not in already
|
||||
for (String id : defaultCategories) {
|
||||
if (!ids.contains(id)) {
|
||||
int numApps = db.getAppDao().getNumberOfAppsInCategory(id);
|
||||
if (numApps > 0) {
|
||||
// name and icon gets set in CategoryController, if not given here
|
||||
Category c = new Category(2L, id, Collections.emptyMap(), Collections.emptyMap(),
|
||||
Collections.emptyMap());
|
||||
CategoryItem item = new CategoryItem(c, numApps);
|
||||
items.add(item);
|
||||
} else {
|
||||
Log.d(TAG, "Not adding default " + id + " because it has no apps.");
|
||||
}
|
||||
}
|
||||
}
|
||||
// sort items
|
||||
LocaleListCompat localeListCompat = LocaleListCompat.getDefault();
|
||||
Collections.sort(items, (o1, o2) -> {
|
||||
ids.add(o2.category.getId());
|
||||
String name1 = o1.category.getName(localeListCompat);
|
||||
if (name1 == null) name1 = o1.category.getId();
|
||||
String name2 = o2.category.getName(localeListCompat);
|
||||
if (name2 == null) name2 = o2.category.getId();
|
||||
return name1.compareToIgnoreCase(name2);
|
||||
});
|
||||
return items;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -57,4 +57,25 @@
|
||||
<item>@string/antinsfw</item>
|
||||
<item>@string/antiothers</item>
|
||||
</string-array>
|
||||
|
||||
<string-array name="defaultCategories">
|
||||
<item>@string/category_Connectivity</item>
|
||||
<item>@string/category_Development</item>
|
||||
<item>@string/category_Games</item>
|
||||
<item>@string/category_Graphics</item>
|
||||
<item>@string/category_Internet</item>
|
||||
<item>@string/category_Money</item>
|
||||
<item>@string/category_Multimedia</item>
|
||||
<item>@string/category_Navigation</item>
|
||||
<item>@string/category_Phone_SMS</item>
|
||||
<item>@string/category_Reading</item>
|
||||
<item>@string/category_Science_Education</item>
|
||||
<item>@string/category_Security</item>
|
||||
<item>@string/category_Sports_Health</item>
|
||||
<item>@string/category_System</item>
|
||||
<item>@string/category_Theming</item>
|
||||
<item>@string/category_Time</item>
|
||||
<item>@string/category_Writing</item>
|
||||
<item>@string/category_Nightly</item>
|
||||
</string-array>
|
||||
</resources>
|
||||
|
||||
@@ -480,7 +480,7 @@ This often occurs with apps installed via Google Play or other sources, if they
|
||||
<string tools:ignore="UnusedResources" name="category_Theming">Theming</string>
|
||||
<string tools:ignore="UnusedResources" name="category_Time">Time</string>
|
||||
<string tools:ignore="UnusedResources" name="category_Writing">Writing</string>
|
||||
<string tools:ignore="UnusedResources" name="category_Nightly">Nightly Builds</string>
|
||||
<string tools:ignore="UnusedResources" name="category_Nightly">nightly</string>
|
||||
|
||||
<plurals name="button_view_all_apps_in_category">
|
||||
<item quantity="one">View %d</item>
|
||||
|
||||
Reference in New Issue
Block a user