mirror of
https://github.com/beemdevelopment/Aegis.git
synced 2025-05-19 16:30:23 +00:00
Introduce support for icon packs
This commit is contained in:
parent
c977b9a064
commit
4f38988c0d
44 changed files with 2128 additions and 54 deletions
|
@ -34,7 +34,10 @@ import com.beemdevelopment.aegis.encoding.Base32;
|
|||
import com.beemdevelopment.aegis.encoding.EncodingException;
|
||||
import com.beemdevelopment.aegis.helpers.DropdownHelper;
|
||||
import com.beemdevelopment.aegis.helpers.EditTextHelper;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.otp.GoogleAuthInfo;
|
||||
import com.beemdevelopment.aegis.otp.HotpInfo;
|
||||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
|
@ -42,23 +45,32 @@ import com.beemdevelopment.aegis.otp.OtpInfoException;
|
|||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.IconPickerDialog;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.util.Cloner;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.beemdevelopment.aegis.vault.VaultManager;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.request.target.CustomTarget;
|
||||
import com.bumptech.glide.request.transition.Transition;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
import com.google.android.material.textfield.TextInputLayout;
|
||||
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import de.hdodenhof.circleimageview.CircleImageView;
|
||||
|
||||
|
@ -72,6 +84,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
private boolean _hasCustomIcon = false;
|
||||
// keep track of icon changes separately as the generated jpeg's are not deterministic
|
||||
private boolean _hasChangedIcon = false;
|
||||
private IconPack.Icon _selectedIcon;
|
||||
private boolean _isEditingIcon;
|
||||
private CircleImageView _iconView;
|
||||
private ImageView _saveImageButton;
|
||||
|
@ -165,9 +178,11 @@ public class EditEntryActivity extends AegisActivity {
|
|||
|
||||
// fill the fields with values if possible
|
||||
if (_origEntry.hasIcon()) {
|
||||
IconViewHelper.setLayerType(_iconView, _origEntry.getIconType());
|
||||
Glide.with(this)
|
||||
.asDrawable()
|
||||
.load(_origEntry)
|
||||
.set(IconLoader.ICON_TYPE, _origEntry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
|
@ -237,7 +252,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
});
|
||||
|
||||
_iconView.setOnClickListener(v -> {
|
||||
startIconSelectionActivity();
|
||||
startIconSelection();
|
||||
});
|
||||
|
||||
_dropdownGroup.setOnItemClickListener(new AdapterView.OnItemClickListener() {
|
||||
|
@ -384,11 +399,13 @@ public class EditEntryActivity extends AegisActivity {
|
|||
});
|
||||
break;
|
||||
case R.id.action_edit_icon:
|
||||
startIconSelectionActivity();
|
||||
startIconSelection();
|
||||
break;
|
||||
case R.id.action_default_icon:
|
||||
TextDrawable drawable = TextDrawableHelper.generate(_origEntry.getIssuer(), _origEntry.getName(), _iconView);
|
||||
_iconView.setImageDrawable(drawable);
|
||||
|
||||
_selectedIcon = null;
|
||||
_hasCustomIcon = false;
|
||||
_hasChangedIcon = true;
|
||||
default:
|
||||
|
@ -398,7 +415,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
return true;
|
||||
}
|
||||
|
||||
private void startIconSelectionActivity() {
|
||||
private void startImageSelectionActivity() {
|
||||
Intent galleryIntent = new Intent(Intent.ACTION_PICK);
|
||||
galleryIntent.setDataAndType(android.provider.MediaStore.Images.Media.INTERNAL_CONTENT_URI, "image/*");
|
||||
|
||||
|
@ -410,6 +427,40 @@ public class EditEntryActivity extends AegisActivity {
|
|||
startActivityForResult(chooserIntent, PICK_IMAGE_REQUEST);
|
||||
}
|
||||
|
||||
private void startIconSelection() {
|
||||
List<IconPack> iconPacks = getApp().getIconPackManager().getIconPacks().stream()
|
||||
.sorted(Comparator.comparing(IconPack::getName))
|
||||
.collect(Collectors.toList());
|
||||
if (iconPacks.size() == 0) {
|
||||
startImageSelectionActivity();
|
||||
return;
|
||||
}
|
||||
|
||||
BottomSheetDialog dialog = IconPickerDialog.create(this, iconPacks, _textIssuer.getText().toString(), new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
_selectedIcon = icon;
|
||||
_hasCustomIcon = true;
|
||||
_hasChangedIcon = true;
|
||||
|
||||
IconViewHelper.setLayerType(_iconView, icon.getIconType());
|
||||
Glide.with(EditEntryActivity.this)
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_iconView);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomSelected() {
|
||||
startImageSelectionActivity();
|
||||
}
|
||||
});
|
||||
Dialogs.showSecureDialog(dialog);
|
||||
}
|
||||
|
||||
private void startEditingIcon(Uri data) {
|
||||
Glide.with(this)
|
||||
.asBitmap()
|
||||
|
@ -438,11 +489,12 @@ public class EditEntryActivity extends AegisActivity {
|
|||
}
|
||||
|
||||
private void stopEditingIcon(boolean save) {
|
||||
if (save) {
|
||||
if (save && _selectedIcon == null) {
|
||||
_iconView.setImageBitmap(_kropView.getCroppedBitmap());
|
||||
}
|
||||
_iconView.setVisibility(View.VISIBLE);
|
||||
_kropView.setVisibility(View.GONE);
|
||||
|
||||
_hasCustomIcon = _hasCustomIcon || save;
|
||||
_hasChangedIcon = save;
|
||||
_isEditingIcon = false;
|
||||
|
@ -577,13 +629,26 @@ public class EditEntryActivity extends AegisActivity {
|
|||
|
||||
if (_hasChangedIcon) {
|
||||
if (_hasCustomIcon) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data);
|
||||
if (_selectedIcon == null) {
|
||||
Bitmap bitmap = ((BitmapDrawable) _iconView.getDrawable()).getBitmap();
|
||||
ByteArrayOutputStream stream = new ByteArrayOutputStream();
|
||||
// the quality parameter is ignored for PNG
|
||||
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream);
|
||||
byte[] data = stream.toByteArray();
|
||||
entry.setIcon(data, IconType.PNG);
|
||||
} else {
|
||||
byte[] iconBytes;
|
||||
try (FileInputStream inStream = new FileInputStream(_selectedIcon.getFile())){
|
||||
iconBytes = IOUtils.readFile(inStream);
|
||||
} catch (IOException e) {
|
||||
// TODO: show dialog
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
|
||||
entry.setIcon(iconBytes, _selectedIcon.getIconType());
|
||||
}
|
||||
} else {
|
||||
entry.setIcon(null);
|
||||
entry.setIcon(null, IconType.INVALID);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -626,7 +691,7 @@ public class EditEntryActivity extends AegisActivity {
|
|||
}
|
||||
}
|
||||
|
||||
private TextWatcher _iconChangeListener = new TextWatcher() {
|
||||
private final TextWatcher _iconChangeListener = new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.os.Bundle;
|
|||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.preference.Preference;
|
||||
import androidx.preference.PreferenceFragmentCompat;
|
||||
|
||||
|
@ -13,7 +14,7 @@ import com.beemdevelopment.aegis.ui.fragments.PreferencesFragment;
|
|||
|
||||
public class PreferencesActivity extends AegisActivity implements
|
||||
PreferenceFragmentCompat.OnPreferenceStartFragmentCallback {
|
||||
private PreferencesFragment _fragment;
|
||||
private Fragment _fragment;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
|
@ -58,18 +59,22 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
|
||||
@Override
|
||||
protected void onRestoreInstanceState(final Bundle inState) {
|
||||
// pass the stored result intent back to the fragment
|
||||
if (inState.containsKey("result")) {
|
||||
_fragment.setResult(inState.getParcelable("result"));
|
||||
if (_fragment instanceof PreferencesFragment) {
|
||||
// pass the stored result intent back to the fragment
|
||||
if (inState.containsKey("result")) {
|
||||
((PreferencesFragment) _fragment).setResult(inState.getParcelable("result"));
|
||||
}
|
||||
}
|
||||
super.onRestoreInstanceState(inState);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(final Bundle outState) {
|
||||
// save the result intent of the fragment
|
||||
// this is done so we don't lose anything if the fragment calls recreate on this activity
|
||||
outState.putParcelable("result", _fragment.getResult());
|
||||
if (_fragment instanceof PreferencesFragment) {
|
||||
// save the result intent of the fragment
|
||||
// this is done so we don't lose anything if the fragment calls recreate on this activity
|
||||
outState.putParcelable("result", ((PreferencesFragment) _fragment).getResult());
|
||||
}
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
|
@ -86,7 +91,7 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
|
||||
@Override
|
||||
public boolean onPreferenceStartFragment(PreferenceFragmentCompat caller, Preference pref) {
|
||||
_fragment = (PreferencesFragment) getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
|
||||
_fragment = getSupportFragmentManager().getFragmentFactory().instantiate(getClassLoader(), pref.getFragment());
|
||||
_fragment.setArguments(pref.getExtras());
|
||||
_fragment.setTargetFragment(caller, 0);
|
||||
showFragment(_fragment);
|
||||
|
@ -95,7 +100,7 @@ public class PreferencesActivity extends AegisActivity implements
|
|||
return true;
|
||||
}
|
||||
|
||||
private void showFragment(PreferencesFragment fragment) {
|
||||
private void showFragment(Fragment fragment) {
|
||||
getSupportFragmentManager().beginTransaction()
|
||||
.setCustomAnimations(R.anim.slide_in_right, R.anim.slide_out_left, R.anim.slide_in_left, R.anim.slide_out_right)
|
||||
.replace(R.id.content, fragment)
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
package com.beemdevelopment.aegis.ui.dialogs;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.FrameLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.widget.PopupMenu;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.ui.views.IconAdapter;
|
||||
import com.beemdevelopment.aegis.ui.views.IconRecyclerView;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
import com.bumptech.glide.RequestBuilder;
|
||||
import com.bumptech.glide.integration.recyclerview.RecyclerViewPreloader;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.bumptech.glide.util.ViewPreloadSizeProvider;
|
||||
import com.google.android.material.bottomsheet.BottomSheetBehavior;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialog;
|
||||
import com.google.android.material.textfield.TextInputEditText;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconPickerDialog {
|
||||
private IconPickerDialog() {
|
||||
|
||||
}
|
||||
|
||||
public static BottomSheetDialog create(Activity activity, List<IconPack> iconPacks, String issuer, IconAdapter.Listener listener) {
|
||||
View view = LayoutInflater.from(activity).inflate(R.layout.dialog_icon_picker, null);
|
||||
TextView textIconPack = view.findViewById(R.id.text_icon_pack);
|
||||
|
||||
BottomSheetDialog dialog = new BottomSheetDialog(activity);
|
||||
dialog.setContentView(view);
|
||||
dialog.create();
|
||||
|
||||
FrameLayout rootView = dialog.findViewById(com.google.android.material.R.id.design_bottom_sheet);
|
||||
rootView.getLayoutParams().height = ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
|
||||
IconAdapter adapter = new IconAdapter(dialog.getContext(), issuer, new IconAdapter.Listener() {
|
||||
@Override
|
||||
public void onIconSelected(IconPack.Icon icon) {
|
||||
dialog.dismiss();
|
||||
listener.onIconSelected(icon);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCustomSelected() {
|
||||
dialog.dismiss();
|
||||
listener.onCustomSelected();
|
||||
}
|
||||
});
|
||||
|
||||
class IconPreloadProvider implements ListPreloader.PreloadModelProvider<IconPack.Icon> {
|
||||
@NonNull
|
||||
@Override
|
||||
public List<IconPack.Icon> getPreloadItems(int position) {
|
||||
IconPack.Icon icon = adapter.getIconAt(position);
|
||||
return Collections.singletonList(icon);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull IconPack.Icon icon) {
|
||||
return Glide.with(dialog.getContext())
|
||||
.asDrawable()
|
||||
.load(icon.getFile())
|
||||
.set(IconLoader.ICON_TYPE, icon.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
}
|
||||
}
|
||||
|
||||
TextInputEditText iconSearch = view.findViewById(R.id.text_search_icon);
|
||||
iconSearch.setOnFocusChangeListener((v, hasFocus) -> {
|
||||
if (hasFocus) {
|
||||
BottomSheetBehavior<FrameLayout> behavior = BottomSheetBehavior.from(rootView);
|
||||
behavior.setState(BottomSheetBehavior.STATE_EXPANDED);
|
||||
}
|
||||
});
|
||||
iconSearch.addTextChangedListener(new TextWatcher() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
String query = s.toString();
|
||||
adapter.setQuery(query.isEmpty() ? null : query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
ViewPreloadSizeProvider<IconPack.Icon> preloadSizeProvider = new ViewPreloadSizeProvider<>();
|
||||
IconPreloadProvider modelProvider = new IconPreloadProvider();
|
||||
RecyclerViewPreloader<IconPack.Icon> preloader = new RecyclerViewPreloader<>(activity, modelProvider, preloadSizeProvider, 10);
|
||||
IconRecyclerView recyclerView = view.findViewById(R.id.list_icons);
|
||||
GridLayoutManager layoutManager = recyclerView.getGridLayoutManager();
|
||||
layoutManager.setSpanSizeLookup(new GridLayoutManager.SpanSizeLookup() {
|
||||
@Override
|
||||
public int getSpanSize(int position) {
|
||||
if (adapter.getItemViewType(position) == R.layout.card_icon) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return recyclerView.getSpanCount();
|
||||
}
|
||||
});
|
||||
recyclerView.setLayoutManager(layoutManager);
|
||||
recyclerView.setAdapter(adapter);
|
||||
recyclerView.addOnScrollListener(preloader);
|
||||
adapter.loadIcons(iconPacks.get(0));
|
||||
textIconPack.setText(iconPacks.get(0).getName());
|
||||
|
||||
view.findViewById(R.id.btn_icon_pack).setOnClickListener(v -> {
|
||||
List<String> iconPackNames = iconPacks.stream()
|
||||
.map(IconPack::getName)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
PopupMenu popupMenu = new PopupMenu(activity, v);
|
||||
popupMenu.setOnMenuItemClickListener(item -> {
|
||||
IconPack pack = iconPacks.get(iconPackNames.indexOf(item.getTitle().toString()));
|
||||
adapter.loadIcons(pack);
|
||||
adapter.setQuery(iconSearch.getText().toString());
|
||||
textIconPack.setText(pack.getName());
|
||||
return true;
|
||||
});
|
||||
Menu menu = popupMenu.getMenu();
|
||||
for (String name : iconPackNames) {
|
||||
menu.add(name);
|
||||
}
|
||||
|
||||
popupMenu.show();
|
||||
});
|
||||
|
||||
return dialog;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,159 @@
|
|||
package com.beemdevelopment.aegis.ui.fragments;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.view.View;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.AegisApplication;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.FabScrollHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconPackException;
|
||||
import com.beemdevelopment.aegis.icons.IconPackExistsException;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.tasks.ImportIconPackTask;
|
||||
import com.beemdevelopment.aegis.ui.views.IconPackAdapter;
|
||||
import com.google.android.material.floatingactionbutton.FloatingActionButton;
|
||||
|
||||
public class IconPacksManagerFragment extends Fragment implements IconPackAdapter.Listener {
|
||||
private static final int CODE_IMPORT = 0;
|
||||
|
||||
private IconPackAdapter _adapter;
|
||||
private IconPackManager _iconPackManager;
|
||||
|
||||
private View _iconPacksView;
|
||||
private RecyclerView _iconPacksRecyclerView;
|
||||
private LinearLayout _noIconPacksView;
|
||||
private FabScrollHelper _fabScrollHelper;
|
||||
|
||||
public IconPacksManagerFragment() {
|
||||
super(R.layout.fragment_icon_packs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
|
||||
_iconPackManager = ((AegisApplication) getContext().getApplicationContext()).getIconPackManager();
|
||||
|
||||
FloatingActionButton fab = view.findViewById(R.id.fab);
|
||||
fab.setOnClickListener(v -> startImportIconPack());
|
||||
_fabScrollHelper = new FabScrollHelper(fab);
|
||||
|
||||
_noIconPacksView = view.findViewById(R.id.vEmptyList);
|
||||
((TextView) view.findViewById(R.id.txt_no_icon_packs)).setMovementMethod(LinkMovementMethod.getInstance());
|
||||
_iconPacksView = view.findViewById(R.id.view_icon_packs);
|
||||
_adapter = new IconPackAdapter(this);
|
||||
_iconPacksRecyclerView = view.findViewById(R.id.list_icon_packs);
|
||||
LinearLayoutManager layoutManager = new LinearLayoutManager(getContext());
|
||||
_iconPacksRecyclerView.setLayoutManager(layoutManager);
|
||||
_iconPacksRecyclerView.setAdapter(_adapter);
|
||||
_iconPacksRecyclerView.setNestedScrollingEnabled(false);
|
||||
_iconPacksRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
_fabScrollHelper.onScroll(dx, dy);
|
||||
}
|
||||
});
|
||||
|
||||
for (IconPack pack : _iconPackManager.getIconPacks()) {
|
||||
_adapter.addIconPack(pack);
|
||||
}
|
||||
|
||||
updateEmptyState();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRemoveIconPack(IconPack pack) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.remove_icon_pack)
|
||||
.setMessage(R.string.remove_icon_pack_description)
|
||||
.setPositiveButton(android.R.string.yes, (dialog, whichButton) -> {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
} catch (IconPackException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
|
||||
return;
|
||||
}
|
||||
_adapter.removeIconPack(pack);
|
||||
updateEmptyState();
|
||||
})
|
||||
.setNegativeButton(android.R.string.no, null)
|
||||
.create());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
|
||||
if (requestCode == CODE_IMPORT && resultCode == Activity.RESULT_OK && data != null && data.getData() != null) {
|
||||
importIconPack(data.getData());
|
||||
}
|
||||
}
|
||||
|
||||
private void importIconPack(Uri uri) {
|
||||
ImportIconPackTask task = new ImportIconPackTask(getContext(), result -> {
|
||||
Exception e = result.getException();
|
||||
if (e instanceof IconPackExistsException) {
|
||||
Dialogs.showSecureDialog(new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.error_occurred)
|
||||
.setMessage(R.string.icon_pack_import_exists_error)
|
||||
.setPositiveButton(R.string.yes, (dialog, which) -> {
|
||||
if (removeIconPack(((IconPackExistsException) e).getIconPack())) {
|
||||
importIconPack(uri);
|
||||
}
|
||||
})
|
||||
.setNegativeButton(R.string.no, null)
|
||||
.create());
|
||||
} else if (e != null) {
|
||||
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_import_error, e);
|
||||
} else {
|
||||
_adapter.addIconPack(result.getIconPack());
|
||||
updateEmptyState();
|
||||
}
|
||||
});
|
||||
task.execute(getLifecycle(), new ImportIconPackTask.Params(_iconPackManager, uri));
|
||||
}
|
||||
|
||||
private boolean removeIconPack(IconPack pack) {
|
||||
try {
|
||||
_iconPackManager.removeIconPack(pack);
|
||||
} catch (IconPackException e) {
|
||||
e.printStackTrace();
|
||||
Dialogs.showErrorDialog(getContext(), R.string.icon_pack_delete_error, e);
|
||||
return false;
|
||||
}
|
||||
|
||||
_adapter.removeIconPack(pack);
|
||||
updateEmptyState();
|
||||
return true;
|
||||
}
|
||||
|
||||
private void startImportIconPack() {
|
||||
Intent intent = new Intent(Intent.ACTION_GET_CONTENT);
|
||||
intent.setType("application/zip");
|
||||
startActivityForResult(intent, CODE_IMPORT);
|
||||
}
|
||||
|
||||
private void updateEmptyState() {
|
||||
if (_adapter.getItemCount() > 0) {
|
||||
_iconPacksView.setVisibility(View.VISIBLE);
|
||||
_noIconPacksView.setVisibility(View.GONE);
|
||||
} else {
|
||||
_iconPacksView.setVisibility(View.GONE);
|
||||
_noIconPacksView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.content.Context;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
|
@ -9,7 +10,9 @@ import com.bumptech.glide.Glide;
|
|||
import com.bumptech.glide.Registry;
|
||||
import com.bumptech.glide.annotation.GlideModule;
|
||||
import com.bumptech.glide.module.AppGlideModule;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
@GlideModule
|
||||
|
@ -17,5 +20,8 @@ public class AegisGlideModule extends AppGlideModule {
|
|||
@Override
|
||||
public void registerComponents(@NonNull Context context, @NonNull Glide glide, @NonNull Registry registry) {
|
||||
registry.prepend(VaultEntry.class, ByteBuffer.class, new IconLoader.Factory());
|
||||
registry.register(SVG.class, PictureDrawable.class, new SvgDrawableTranscoder())
|
||||
.append(InputStream.class, SVG.class, new SvgDecoder())
|
||||
.append(ByteBuffer.class, SVG.class, new SvgBytesDecoder());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,11 @@ package com.beemdevelopment.aegis.ui.glide;
|
|||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Priority;
|
||||
import com.bumptech.glide.load.DataSource;
|
||||
import com.bumptech.glide.load.Option;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.data.DataFetcher;
|
||||
import com.bumptech.glide.load.model.ModelLoader;
|
||||
|
@ -14,6 +16,8 @@ import com.bumptech.glide.load.model.MultiModelLoaderFactory;
|
|||
import java.nio.ByteBuffer;
|
||||
|
||||
public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
||||
public static final Option<IconType> ICON_TYPE = Option.memory("ICON_TYPE", IconType.INVALID);
|
||||
|
||||
@Override
|
||||
public LoadData<ByteBuffer> buildLoadData(@NonNull VaultEntry model, int width, int height, @NonNull Options options) {
|
||||
return new LoadData<>(new UUIDKey(model.getUUID()), new Fetcher(model));
|
||||
|
@ -25,7 +29,7 @@ public class IconLoader implements ModelLoader<VaultEntry, ByteBuffer> {
|
|||
}
|
||||
|
||||
public static class Fetcher implements DataFetcher<ByteBuffer> {
|
||||
private VaultEntry _model;
|
||||
private final VaultEntry _model;
|
||||
|
||||
private Fetcher(VaultEntry model) {
|
||||
_model = model;
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
|
||||
public class SvgBytesDecoder implements ResourceDecoder<ByteBuffer, SVG> {
|
||||
private SvgDecoder _decoder = new SvgDecoder();
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull ByteBuffer source, @NonNull Options options) throws IOException {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
|
||||
return _decoder.handles(inStream, options);
|
||||
}
|
||||
}
|
||||
|
||||
public Resource<SVG> decode(@NonNull ByteBuffer source, int width, int height, @NonNull Options options) throws IOException {
|
||||
try (ByteArrayInputStream inStream = new ByteArrayInputStream(source.array())) {
|
||||
return _decoder.decode(inStream, width, height, options);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.ResourceDecoder;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
import com.caverock.androidsvg.SVGParseException;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
import static com.bumptech.glide.request.target.Target.SIZE_ORIGINAL;
|
||||
|
||||
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
|
||||
|
||||
/** Decodes an SVG internal representation from an {@link InputStream}. */
|
||||
public class SvgDecoder implements ResourceDecoder<InputStream, SVG> {
|
||||
|
||||
@Override
|
||||
public boolean handles(@NonNull InputStream source, @NonNull Options options) {
|
||||
return options.get(IconLoader.ICON_TYPE) == IconType.SVG;
|
||||
}
|
||||
|
||||
public Resource<SVG> decode(
|
||||
@NonNull InputStream source, int width, int height, @NonNull Options options)
|
||||
throws IOException {
|
||||
try {
|
||||
SVG svg = SVG.getFromInputStream(source);
|
||||
if (width != SIZE_ORIGINAL) {
|
||||
svg.setDocumentWidth(width);
|
||||
}
|
||||
if (height != SIZE_ORIGINAL) {
|
||||
svg.setDocumentHeight(height);
|
||||
}
|
||||
return new SimpleResource<>(svg);
|
||||
} catch (SVGParseException ex) {
|
||||
throw new IOException("Cannot load SVG from stream", ex);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package com.beemdevelopment.aegis.ui.glide;
|
||||
|
||||
import android.graphics.Picture;
|
||||
import android.graphics.drawable.PictureDrawable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import com.bumptech.glide.load.Options;
|
||||
import com.bumptech.glide.load.engine.Resource;
|
||||
import com.bumptech.glide.load.resource.SimpleResource;
|
||||
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder;
|
||||
import com.caverock.androidsvg.SVG;
|
||||
|
||||
// source: https://github.com/bumptech/glide/tree/master/samples/svg/src/main/java/com/bumptech/glide/samples/svg
|
||||
|
||||
/**
|
||||
* Convert the {@link SVG}'s internal representation to an Android-compatible one ({@link Picture}).
|
||||
*/
|
||||
public class SvgDrawableTranscoder implements ResourceTranscoder<SVG, PictureDrawable> {
|
||||
@Nullable
|
||||
@Override
|
||||
public Resource<PictureDrawable> transcode(
|
||||
@NonNull Resource<SVG> toTranscode, @NonNull Options options) {
|
||||
SVG svg = toTranscode.get();
|
||||
Picture picture = svg.renderToPicture();
|
||||
PictureDrawable drawable = new PictureDrawable(picture);
|
||||
return new SimpleResource<>(drawable);
|
||||
}
|
||||
}
|
|
@ -35,6 +35,7 @@ public class ImportFileTask extends ProgressDialogTask<Uri, ImportFileTask.Resul
|
|||
|
||||
return new Result(tempFile, null);
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
return new Result(null, e);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,95 @@
|
|||
package com.beemdevelopment.aegis.ui.tasks;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconPackException;
|
||||
import com.beemdevelopment.aegis.icons.IconPackManager;
|
||||
import com.beemdevelopment.aegis.util.IOUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
public class ImportIconPackTask extends ProgressDialogTask<ImportIconPackTask.Params, ImportIconPackTask.Result> {
|
||||
private final ImportIconPackTask.Callback _cb;
|
||||
|
||||
public ImportIconPackTask(Context context, ImportIconPackTask.Callback cb) {
|
||||
super(context, context.getString(R.string.importing_icon_pack));
|
||||
_cb = cb;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ImportIconPackTask.Result doInBackground(ImportIconPackTask.Params... params) {
|
||||
Context context = getDialog().getContext();
|
||||
ImportIconPackTask.Params param = params[0];
|
||||
|
||||
File tempFile = null;
|
||||
try {
|
||||
tempFile = File.createTempFile("icon-pack-", "", context.getCacheDir());
|
||||
try (InputStream inStream = context.getContentResolver().openInputStream(param.getUri());
|
||||
FileOutputStream outStream = new FileOutputStream(tempFile)) {
|
||||
IOUtils.copy(inStream, outStream);
|
||||
}
|
||||
|
||||
IconPack pack = param.getManager().importPack(tempFile);
|
||||
return new Result(pack, null);
|
||||
} catch (IOException | IconPackException e) {
|
||||
e.printStackTrace();
|
||||
return new ImportIconPackTask.Result(null, e);
|
||||
} finally {
|
||||
if (tempFile != null) {
|
||||
tempFile.delete();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onPostExecute(ImportIconPackTask.Result result) {
|
||||
super.onPostExecute(result);
|
||||
_cb.onTaskFinished(result);
|
||||
}
|
||||
|
||||
public interface Callback {
|
||||
void onTaskFinished(ImportIconPackTask.Result result);
|
||||
}
|
||||
|
||||
public static class Params {
|
||||
private final IconPackManager _manager;
|
||||
private final Uri _uri;
|
||||
|
||||
public Params(IconPackManager manager, Uri uri) {
|
||||
_manager = manager;
|
||||
_uri = uri;
|
||||
}
|
||||
|
||||
public IconPackManager getManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public Uri getUri() {
|
||||
return _uri;
|
||||
}
|
||||
}
|
||||
|
||||
public static class Result {
|
||||
private final IconPack _pack;
|
||||
private final Exception _e;
|
||||
|
||||
public Result(IconPack pack, Exception e) {
|
||||
_pack = pack;
|
||||
_e = e;
|
||||
}
|
||||
|
||||
public IconPack getIconPack() {
|
||||
return _pack;
|
||||
}
|
||||
|
||||
public Exception getException() {
|
||||
return _e;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -14,6 +14,7 @@ import androidx.recyclerview.widget.RecyclerView;
|
|||
|
||||
import com.amulyakhare.textdrawable.TextDrawable;
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.TextDrawableHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
|
@ -21,10 +22,10 @@ import com.beemdevelopment.aegis.otp.HotpInfo;
|
|||
import com.beemdevelopment.aegis.otp.OtpInfo;
|
||||
import com.beemdevelopment.aegis.otp.SteamInfo;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
import com.google.zxing.common.StringUtils;
|
||||
|
||||
public class EntryHolder extends RecyclerView.ViewHolder {
|
||||
private static final float DEFAULT_ALPHA = 1.0f;
|
||||
|
@ -141,9 +142,11 @@ public class EntryHolder extends RecyclerView.ViewHolder {
|
|||
|
||||
public void loadIcon(Fragment fragment) {
|
||||
if (_entry.hasIcon()) {
|
||||
IconViewHelper.setLayerType(_profileDrawable, _entry.getIconType());
|
||||
Glide.with(fragment)
|
||||
.asDrawable()
|
||||
.load(_entry)
|
||||
.set(IconLoader.ICON_TYPE, _entry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_profileDrawable);
|
||||
|
|
|
@ -4,6 +4,7 @@ import android.annotation.SuppressLint;
|
|||
import android.content.Context;
|
||||
import android.content.res.ColorStateList;
|
||||
import android.graphics.Rect;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.MotionEvent;
|
||||
|
@ -31,6 +32,7 @@ import com.beemdevelopment.aegis.helpers.SimpleItemTouchHelperCallback;
|
|||
import com.beemdevelopment.aegis.helpers.UiRefresher;
|
||||
import com.beemdevelopment.aegis.otp.TotpInfo;
|
||||
import com.beemdevelopment.aegis.ui.dialogs.Dialogs;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.beemdevelopment.aegis.vault.VaultEntry;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.ListPreloader;
|
||||
|
@ -486,10 +488,11 @@ public class EntryListView extends Fragment implements EntryAdapter.Listener {
|
|||
|
||||
@Nullable
|
||||
@Override
|
||||
public RequestBuilder getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
public RequestBuilder<Drawable> getPreloadRequestBuilder(@NonNull VaultEntry entry) {
|
||||
return Glide.with(EntryListView.this)
|
||||
.asDrawable()
|
||||
.load(entry)
|
||||
.set(IconLoader.ICON_TYPE, entry.getIconType())
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,288 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
public class IconAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
|
||||
private final Context _context;
|
||||
private final String _issuer;
|
||||
private final Listener _listener;
|
||||
private IconPack _pack;
|
||||
private List<IconPack.Icon> _icons;
|
||||
private final List<CategoryHeader> _categories;
|
||||
private String _query;
|
||||
|
||||
public IconAdapter(@NonNull Context context, String issuer, @NonNull Listener listener) {
|
||||
_context = context;
|
||||
_issuer = issuer;
|
||||
_listener = listener;
|
||||
_icons = new ArrayList<>();
|
||||
_categories = new ArrayList<>();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads all icons from the given icon pack into this adapter. Any icons added before this call will be overwritten.
|
||||
*/
|
||||
public void loadIcons(IconPack pack) {
|
||||
_pack = pack;
|
||||
_query = null;
|
||||
_icons = new ArrayList<>(_pack.getIcons());
|
||||
_categories.clear();
|
||||
|
||||
Comparator<IconPack.Icon> iconCategoryComparator = (i1, i2) -> {
|
||||
String c1 = getCategoryString(i1.getCategory());
|
||||
String c2 = getCategoryString(i2.getCategory());
|
||||
return c1.compareTo(c2);
|
||||
};
|
||||
Collections.sort(_icons, iconCategoryComparator.thenComparing(IconPack.Icon::getName));
|
||||
|
||||
long categoryCount = _icons.stream()
|
||||
.map(IconPack.Icon::getCategory)
|
||||
.filter(Objects::nonNull)
|
||||
.distinct()
|
||||
.count();
|
||||
|
||||
List<IconPack.Icon> suggested = pack.getSuggestedIcons(_issuer);
|
||||
suggested.add(0, new DummyIcon(_context.getString(R.string.icon_custom)));
|
||||
if (suggested.size() > 0) {
|
||||
CategoryHeader category = new CategoryHeader(_context.getString(R.string.suggested));
|
||||
category.setIsCollapsed(false);
|
||||
category.getIcons().addAll(suggested);
|
||||
_categories.add(category);
|
||||
}
|
||||
|
||||
CategoryHeader category = null;
|
||||
for (IconPack.Icon icon : _icons) {
|
||||
String iconCategory = getCategoryString(icon.getCategory());
|
||||
if (category == null || !getCategoryString(category.getCategory()).equals(iconCategory)) {
|
||||
boolean collapsed = !(categoryCount == 0 && category == null);
|
||||
category = new CategoryHeader(iconCategory);
|
||||
category.setIsCollapsed(collapsed);
|
||||
_categories.add(category);
|
||||
}
|
||||
|
||||
category.getIcons().add(icon);
|
||||
}
|
||||
|
||||
_icons.addAll(0, suggested);
|
||||
updateCategoryPositions();
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public void setQuery(@Nullable String query) {
|
||||
_query = query;
|
||||
|
||||
if (_query == null) {
|
||||
loadIcons(_pack);
|
||||
} else {
|
||||
_icons = _pack.getIcons().stream()
|
||||
.filter(i -> i.isSuggestedFor(query))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
Collections.sort(_icons, Comparator.comparing(IconPack.Icon::getName));
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
public IconPack.Icon getIconAt(int position) {
|
||||
if (isQueryActive()) {
|
||||
return _icons.get(position);
|
||||
}
|
||||
|
||||
position = translateIconPosition(position);
|
||||
return _icons.get(position);
|
||||
}
|
||||
|
||||
public CategoryHeader getCategoryAt(int position) {
|
||||
return _categories.stream()
|
||||
.filter(c -> c.getPosition() == position)
|
||||
.findFirst()
|
||||
.orElse(null);
|
||||
}
|
||||
|
||||
private String getCategoryString(String category) {
|
||||
return category == null ? _context.getString(R.string.uncategorized) : category;
|
||||
}
|
||||
|
||||
private boolean isCategoryPosition(int position) {
|
||||
if (isQueryActive()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return getCategoryAt(position) != null;
|
||||
}
|
||||
|
||||
private int translateIconPosition(int position) {
|
||||
int offset = 0;
|
||||
for (CategoryHeader category : _categories) {
|
||||
if (category.getPosition() < position) {
|
||||
offset++;
|
||||
if (category.isCollapsed()) {
|
||||
offset -= category.getIcons().size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return position - offset;
|
||||
}
|
||||
|
||||
private void updateCategoryPositions() {
|
||||
int i = 0;
|
||||
for (CategoryHeader category : _categories) {
|
||||
category.setPosition(i);
|
||||
|
||||
int icons = 0;
|
||||
if (!category.isCollapsed()) {
|
||||
icons = category.getIcons().size();
|
||||
}
|
||||
|
||||
i += 1 + icons;
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(viewType, parent, false);
|
||||
return viewType == R.layout.card_icon ? new IconHolder(view) : new IconCategoryHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) {
|
||||
if (!isCategoryPosition(position)) {
|
||||
IconHolder iconHolder = (IconHolder) holder;
|
||||
IconPack.Icon icon = getIconAt(position);
|
||||
iconHolder.setData(icon);
|
||||
iconHolder.loadIcon(_context);
|
||||
iconHolder.itemView.setOnClickListener(v -> {
|
||||
if (icon instanceof DummyIcon) {
|
||||
_listener.onCustomSelected();
|
||||
} else {
|
||||
_listener.onIconSelected(icon);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
IconCategoryHolder categoryHolder = (IconCategoryHolder) holder;
|
||||
CategoryHeader category = getCategoryAt(position);
|
||||
categoryHolder.setData(category);
|
||||
categoryHolder.itemView.setOnClickListener(v -> {
|
||||
boolean collapsed = !category.isCollapsed();
|
||||
categoryHolder.setIsCollapsed(collapsed);
|
||||
category.setIsCollapsed(collapsed);
|
||||
|
||||
int startPosition = category.getPosition() + 1;
|
||||
if (category.isCollapsed()) {
|
||||
notifyItemRangeRemoved(startPosition, category.getIcons().size());
|
||||
} else {
|
||||
notifyItemRangeInserted(startPosition, category.getIcons().size());
|
||||
}
|
||||
|
||||
updateCategoryPositions();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
if (isQueryActive()) {
|
||||
return _icons.size();
|
||||
}
|
||||
|
||||
int items = _categories.stream()
|
||||
.filter(c -> !c.isCollapsed())
|
||||
.mapToInt(c -> c.getIcons().size())
|
||||
.sum();
|
||||
|
||||
return items + _categories.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
if (isCategoryPosition(position)) {
|
||||
return R.layout.card_icon_category;
|
||||
}
|
||||
|
||||
return R.layout.card_icon;
|
||||
}
|
||||
|
||||
private boolean isQueryActive() {
|
||||
return _query != null;
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onIconSelected(IconPack.Icon icon);
|
||||
void onCustomSelected();
|
||||
}
|
||||
|
||||
public static class DummyIcon extends IconPack.Icon {
|
||||
private final String _name;
|
||||
|
||||
protected DummyIcon(String name) {
|
||||
super(null, null, null);
|
||||
_name = name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return _name;
|
||||
}
|
||||
|
||||
@Override
|
||||
public IconType getIconType() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public static class CategoryHeader {
|
||||
private final String _category;
|
||||
private int _position = -1;
|
||||
private final List<IconPack.Icon> _icons;
|
||||
private boolean _collapsed = true;
|
||||
|
||||
public CategoryHeader(String category) {
|
||||
_category = category;
|
||||
_icons = new ArrayList<>();
|
||||
}
|
||||
|
||||
public String getCategory() {
|
||||
return _category;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return _position;
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
_position = position;
|
||||
}
|
||||
|
||||
public List<IconPack.Icon> getIcons() {
|
||||
return _icons;
|
||||
}
|
||||
|
||||
public boolean isCollapsed() {
|
||||
return _collapsed;
|
||||
}
|
||||
|
||||
public void setIsCollapsed(boolean collapsed) {
|
||||
_collapsed = collapsed;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,36 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
|
||||
public class IconCategoryHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView _textView;
|
||||
private final ImageView _imgView;
|
||||
|
||||
public IconCategoryHolder(final View view) {
|
||||
super(view);
|
||||
_textView = view.findViewById(R.id.icon_category);
|
||||
_imgView = view.findViewById(R.id.icon_category_indicator);
|
||||
}
|
||||
|
||||
public void setData(IconAdapter.CategoryHeader header) {
|
||||
_textView.setText(header.getCategory());
|
||||
_imgView.setRotation(getRotation(header.isCollapsed()));
|
||||
}
|
||||
|
||||
public void setIsCollapsed(boolean collapsed) {
|
||||
_imgView.animate()
|
||||
.setDuration(200)
|
||||
.rotation(getRotation(collapsed))
|
||||
.start();
|
||||
}
|
||||
|
||||
private static int getRotation(boolean collapsed) {
|
||||
return collapsed ? 90 : 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,59 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.helpers.IconViewHelper;
|
||||
import com.beemdevelopment.aegis.helpers.ThemeHelper;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
import com.beemdevelopment.aegis.icons.IconType;
|
||||
import com.beemdevelopment.aegis.ui.glide.IconLoader;
|
||||
import com.bumptech.glide.Glide;
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class IconHolder extends RecyclerView.ViewHolder {
|
||||
private File _iconFile;
|
||||
private IconType _iconType;
|
||||
private boolean _isCustom;
|
||||
|
||||
private final ImageView _imageView;
|
||||
private final TextView _textView;
|
||||
|
||||
public IconHolder(final View view) {
|
||||
super(view);
|
||||
_imageView = view.findViewById(R.id.icon);
|
||||
_textView = view.findViewById(R.id.icon_name);
|
||||
}
|
||||
|
||||
public void setData(IconPack.Icon icon) {
|
||||
_iconFile = icon.getFile();
|
||||
_iconType = icon.getIconType();
|
||||
_isCustom = icon instanceof IconAdapter.DummyIcon;
|
||||
_textView.setText(icon.getName());
|
||||
}
|
||||
|
||||
public void loadIcon(Context context) {
|
||||
if (_isCustom) {
|
||||
int tint = ThemeHelper.getThemeColor(R.attr.iconColorPrimary, context.getTheme());
|
||||
_imageView.setColorFilter(tint);
|
||||
_imageView.setImageResource(R.drawable.ic_plus_black_24dp);
|
||||
} else {
|
||||
_imageView.setImageTintList(null);
|
||||
IconViewHelper.setLayerType(_imageView, _iconType);
|
||||
Glide.with(context)
|
||||
.asDrawable()
|
||||
.load(_iconFile)
|
||||
.set(IconLoader.ICON_TYPE, _iconType)
|
||||
.diskCacheStrategy(DiskCacheStrategy.NONE)
|
||||
.skipMemoryCache(false)
|
||||
.into(_imageView);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
public class IconPackAdapter extends RecyclerView.Adapter<IconPackHolder> {
|
||||
private IconPackAdapter.Listener _listener;
|
||||
private List<IconPack> _iconPacks;
|
||||
|
||||
public IconPackAdapter(IconPackAdapter.Listener listener) {
|
||||
_listener = listener;
|
||||
_iconPacks = new ArrayList<>();
|
||||
}
|
||||
|
||||
public void addIconPack(IconPack pack) {
|
||||
_iconPacks.add(pack);
|
||||
|
||||
int position = getItemCount() - 1;
|
||||
if (position == 0) {
|
||||
notifyDataSetChanged();
|
||||
} else {
|
||||
notifyItemInserted(position);
|
||||
}
|
||||
}
|
||||
|
||||
public void removeIconPack(IconPack pack) {
|
||||
int position = _iconPacks.indexOf(pack);
|
||||
_iconPacks.remove(position);
|
||||
notifyItemRemoved(position);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public IconPackHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.card_icon_pack, parent, false);
|
||||
return new IconPackHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull IconPackHolder holder, int position) {
|
||||
holder.setData(_iconPacks.get(position));
|
||||
holder.setOnDeleteClickListener(v -> {
|
||||
int position12 = holder.getAdapterPosition();
|
||||
_listener.onRemoveIconPack(_iconPacks.get(position12));
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return _iconPacks.size();
|
||||
}
|
||||
|
||||
public interface Listener {
|
||||
void onRemoveIconPack(IconPack pack);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
import com.beemdevelopment.aegis.R;
|
||||
import com.beemdevelopment.aegis.icons.IconPack;
|
||||
|
||||
public class IconPackHolder extends RecyclerView.ViewHolder {
|
||||
private final TextView _iconPackName;
|
||||
private final TextView _iconPackInfo;
|
||||
private final ImageView _buttonDelete;
|
||||
|
||||
public IconPackHolder(final View view) {
|
||||
super(view);
|
||||
_iconPackName = view.findViewById(R.id.text_icon_pack_name);
|
||||
_iconPackInfo = view.findViewById(R.id.text_icon_pack_info);
|
||||
_buttonDelete = view.findViewById(R.id.button_delete);
|
||||
}
|
||||
|
||||
public void setData(IconPack pack) {
|
||||
_iconPackName.setText(String.format("%s (v%d)", pack.getName(), pack.getVersion()));
|
||||
_iconPackInfo.setText(itemView.getResources().getQuantityString(R.plurals.icon_pack_info, pack.getIcons().size(), pack.getIcons().size()));
|
||||
}
|
||||
|
||||
public void setOnDeleteClickListener(View.OnClickListener listener) {
|
||||
_buttonDelete.setOnClickListener(listener);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
package com.beemdevelopment.aegis.ui.views;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.util.AttributeSet;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.recyclerview.widget.GridLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
// source (slightly modified for Aegis): https://github.com/chiuki/android-recyclerview/blob/745dc88/app/src/main/java/com/sqisland/android/recyclerview/AutofitRecyclerView.java
|
||||
public class IconRecyclerView extends RecyclerView {
|
||||
private GridLayoutManager _manager;
|
||||
private int _columnWidth = -1;
|
||||
private int _spanCount;
|
||||
|
||||
public IconRecyclerView(@NonNull Context context) {
|
||||
super(context);
|
||||
init(context, null);
|
||||
}
|
||||
|
||||
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
public IconRecyclerView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
|
||||
super(context, attrs, defStyleAttr);
|
||||
init(context, attrs);
|
||||
}
|
||||
|
||||
private void init(Context context, AttributeSet attrs) {
|
||||
if (attrs != null) {
|
||||
int[] attrsArray = {
|
||||
android.R.attr.columnWidth
|
||||
};
|
||||
TypedArray array = context.obtainStyledAttributes(attrs, attrsArray);
|
||||
_columnWidth = array.getDimensionPixelSize(0, -1);
|
||||
array.recycle();
|
||||
}
|
||||
|
||||
_manager = new GridLayoutManager(getContext(), 1);
|
||||
setLayoutManager(_manager);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onMeasure(int widthSpec, int heightSpec) {
|
||||
super.onMeasure(widthSpec, heightSpec);
|
||||
if (_columnWidth > 0) {
|
||||
_spanCount = Math.max(1, getMeasuredWidth() / _columnWidth);
|
||||
_manager.setSpanCount(_spanCount);
|
||||
}
|
||||
}
|
||||
|
||||
public GridLayoutManager getGridLayoutManager() {
|
||||
return _manager;
|
||||
}
|
||||
|
||||
public int getSpanCount() {
|
||||
return _spanCount;
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue