Allow to reorder and rename categories

This commit is contained in:
inorichi 2015-12-28 18:06:07 +01:00
parent e548cbf171
commit 3f1f9ea9f2
20 changed files with 280 additions and 20 deletions

View file

@ -351,6 +351,7 @@ public class DatabaseHelper {
.listOfObjects(Category.class) .listOfObjects(Category.class)
.withQuery(Query.builder() .withQuery(Query.builder()
.table(CategoryTable.TABLE) .table(CategoryTable.TABLE)
.orderBy(CategoryTable.COLUMN_ORDER)
.build()) .build())
.prepare(); .prepare();
} }
@ -361,6 +362,12 @@ public class DatabaseHelper {
.prepare(); .prepare();
} }
public PreparedPutCollectionOfObjects<Category> insertCategories(List<Category> categories) {
return db.put()
.objects(categories)
.prepare();
}
public PreparedDeleteObject<Category> deleteCategory(Category category) { public PreparedDeleteObject<Category> deleteCategory(Category category) {
return db.delete() return db.delete()
.object(category) .object(category)

View file

@ -16,6 +16,12 @@ public class Category implements Serializable {
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME) @StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
public String name; public String name;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
public int order;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
public int flags;
public Category() {} public Category() {}
public static Category create(String name) { public static Category create(String name) {

View file

@ -13,6 +13,12 @@ public class CategoryTable {
@NonNull @NonNull
public static final String COLUMN_NAME = "name"; public static final String COLUMN_NAME = "name";
@NonNull
public static final String COLUMN_ORDER = "sort";
@NonNull
public static final String COLUMN_FLAGS = "flags";
// This is just class with Meta Data, we don't need instances // This is just class with Meta Data, we don't need instances
private CategoryTable() { private CategoryTable() {
throw new IllegalStateException("No instances please"); throw new IllegalStateException("No instances please");
@ -24,7 +30,9 @@ public class CategoryTable {
public static String getCreateTableQuery() { public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "(" return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, " + COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_NAME + " TEXT NOT NULL" + COLUMN_NAME + " TEXT NOT NULL, "
+ COLUMN_ORDER + " INTEGER NOT NULL, "
+ COLUMN_FLAGS + " INTEGER NOT NULL"
+ ");"; + ");";
} }

View file

@ -0,0 +1,57 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.kanade.mangafeed.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
/**
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
*
* @author Paul Burke (ipaulpro)
*/
public interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemMove(int fromPosition, int toPosition);
/**
* Called when an item has been dismissed by a swipe.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemDismiss(int position);
}

View file

@ -0,0 +1,13 @@
package eu.kanade.mangafeed.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
public interface OnStartDragListener {
/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/
void onStartDrag(RecyclerView.ViewHolder viewHolder);
}

View file

@ -0,0 +1,43 @@
package eu.kanade.mangafeed.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final ItemTouchHelperAdapter adapter;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
this.adapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
adapter.onItemDismiss(viewHolder.getAdapterPosition());
}
}

View file

@ -6,25 +6,29 @@ import android.view.ViewGroup;
import com.amulyakhare.textdrawable.util.ColorGenerator; import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List; import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.Category; import eu.kanade.mangafeed.data.database.models.Category;
import eu.kanade.mangafeed.ui.base.adapter.ItemTouchHelperAdapter;
public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> { public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> implements
ItemTouchHelperAdapter {
private CategoryFragment fragment; private final CategoryFragment fragment;
private ColorGenerator generator; private final ColorGenerator generator;
public CategoryAdapter(CategoryFragment fragment) { public CategoryAdapter(CategoryFragment fragment) {
this.fragment = fragment; this.fragment = fragment;
setHasStableIds(true);
generator = ColorGenerator.DEFAULT; generator = ColorGenerator.DEFAULT;
setHasStableIds(true);
} }
public void setItems(List<Category> items) { public void setItems(List<Category> items) {
mItems = items; mItems = new ArrayList<>(items);
notifyDataSetChanged(); notifyDataSetChanged();
} }
@ -42,7 +46,7 @@ public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> {
public CategoryHolder onCreateViewHolder(ViewGroup parent, int viewType) { public CategoryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(fragment.getActivity()); LayoutInflater inflater = LayoutInflater.from(fragment.getActivity());
View v = inflater.inflate(R.layout.item_edit_categories, parent, false); View v = inflater.inflate(R.layout.item_edit_categories, parent, false);
return new CategoryHolder(v, this, fragment); return new CategoryHolder(v, this, fragment, fragment);
} }
@Override @Override
@ -54,7 +58,23 @@ public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> {
holder.itemView.setActivated(isSelected(position)); holder.itemView.setActivated(isSelected(position));
} }
public ColorGenerator getColorGenerator() { @Override
return generator; public void onItemMove(int fromPosition, int toPosition) {
if (fromPosition < toPosition) {
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(mItems, i, i + 1);
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(mItems, i, i - 1);
}
}
fragment.getPresenter().reorderCategories(mItems);
}
@Override
public void onItemDismiss(int position) {
} }
} }

View file

@ -6,6 +6,7 @@ import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.view.ActionMode; import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
@ -22,6 +23,7 @@ import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.Category; import eu.kanade.mangafeed.data.database.models.Category;
import eu.kanade.mangafeed.ui.base.activity.BaseActivity; import eu.kanade.mangafeed.ui.base.activity.BaseActivity;
import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder; import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.mangafeed.ui.base.adapter.OnStartDragListener;
import eu.kanade.mangafeed.ui.base.fragment.BaseRxFragment; import eu.kanade.mangafeed.ui.base.fragment.BaseRxFragment;
import eu.kanade.mangafeed.ui.decoration.DividerItemDecoration; import eu.kanade.mangafeed.ui.decoration.DividerItemDecoration;
import eu.kanade.mangafeed.ui.library.LibraryCategoryAdapter; import eu.kanade.mangafeed.ui.library.LibraryCategoryAdapter;
@ -29,14 +31,15 @@ import nucleus.factory.RequiresPresenter;
import rx.Observable; import rx.Observable;
@RequiresPresenter(CategoryPresenter.class) @RequiresPresenter(CategoryPresenter.class)
public class CategoryFragment extends BaseRxFragment<CategoryPresenter> public class CategoryFragment extends BaseRxFragment<CategoryPresenter> implements
implements ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
@Bind(R.id.categories_list) RecyclerView recycler; @Bind(R.id.categories_list) RecyclerView recycler;
@Bind(R.id.fab) FloatingActionButton fab; @Bind(R.id.fab) FloatingActionButton fab;
private CategoryAdapter adapter; private CategoryAdapter adapter;
private ActionMode actionMode; private ActionMode actionMode;
private ItemTouchHelper touchHelper;
public static CategoryFragment newInstance() { public static CategoryFragment newInstance() {
return new CategoryFragment(); return new CategoryFragment();
@ -56,12 +59,17 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
recycler.addItemDecoration(new DividerItemDecoration( recycler.addItemDecoration(new DividerItemDecoration(
ResourcesCompat.getDrawable(getResources(), R.drawable.line_divider, null))); ResourcesCompat.getDrawable(getResources(), R.drawable.line_divider, null)));
// Touch helper to drag and reorder categories
touchHelper = new ItemTouchHelper(new CategoryItemTouchHelper(adapter));
touchHelper.attachToRecyclerView(recycler);
fab.setOnClickListener(v -> { fab.setOnClickListener(v -> {
new MaterialDialog.Builder(getActivity()) new MaterialDialog.Builder(getActivity())
.title(R.string.action_add_category) .title(R.string.action_add_category)
.input(R.string.name, 0, false, (dialog, input) -> { .input(R.string.name, 0, false, (dialog, input) -> {
getPresenter().createCategory(input.toString()); getPresenter().createCategory(input.toString());
}).show(); })
.show();
}); });
return view; return view;
@ -105,6 +113,8 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
} else { } else {
setContextTitle(count); setContextTitle(count);
actionMode.invalidate(); actionMode.invalidate();
MenuItem editItem = actionMode.getMenu().findItem(R.id.action_edit);
editItem.setVisible(count == 1);
} }
} }
@ -128,7 +138,10 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_delete: case R.id.action_delete:
getPresenter().deleteCategories(getSelectedCategories()); deleteCategories(getSelectedCategories());
return true;
case R.id.action_edit:
editCategory(getSelectedCategories().get(0));
return true; return true;
} }
return false; return false;
@ -147,4 +160,22 @@ public class CategoryFragment extends BaseRxFragment<CategoryPresenter>
} }
} }
private void deleteCategories(List<Category> categories) {
getPresenter().deleteCategories(categories);
}
private void editCategory(Category category) {
new MaterialDialog.Builder(getActivity())
.title(R.string.action_rename_category)
.input(getString(R.string.name), category.name, false, (dialog, input) -> {
getPresenter().renameCategory(category, input.toString());
})
.show();
}
@Override
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
touchHelper.startDrag(viewHolder);
}
} }

View file

@ -1,5 +1,7 @@
package eu.kanade.mangafeed.ui.library.category; package eu.kanade.mangafeed.ui.library.category;
import android.support.v4.view.MotionEventCompat;
import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -13,6 +15,7 @@ import butterknife.OnClick;
import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.R;
import eu.kanade.mangafeed.data.database.models.Category; import eu.kanade.mangafeed.data.database.models.Category;
import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder; import eu.kanade.mangafeed.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.mangafeed.ui.base.adapter.OnStartDragListener;
public class CategoryHolder extends FlexibleViewHolder { public class CategoryHolder extends FlexibleViewHolder {
@ -20,11 +23,21 @@ public class CategoryHolder extends FlexibleViewHolder {
@Bind(R.id.image) ImageView image; @Bind(R.id.image) ImageView image;
@Bind(R.id.title) TextView title; @Bind(R.id.title) TextView title;
@Bind(R.id.reorder) ImageView reorder;
public CategoryHolder(View view, CategoryAdapter adapter, OnListItemClickListener listener) { public CategoryHolder(View view, CategoryAdapter adapter,
OnListItemClickListener listener, OnStartDragListener dragListener) {
super(view, adapter, listener); super(view, adapter, listener);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
this.view = view; this.view = view;
reorder.setOnTouchListener((v, event) -> {
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
dragListener.onStartDrag(this);
return true;
}
return false;
});
} }
public void onSetValues(Category category, ColorGenerator generator) { public void onSetValues(Category category, ColorGenerator generator) {

View file

@ -0,0 +1,16 @@
package eu.kanade.mangafeed.ui.library.category;
import eu.kanade.mangafeed.ui.base.adapter.ItemTouchHelperAdapter;
import eu.kanade.mangafeed.ui.base.adapter.SimpleItemTouchHelperCallback;
public class CategoryItemTouchHelper extends SimpleItemTouchHelperCallback {
public CategoryItemTouchHelper(ItemTouchHelperAdapter adapter) {
super(adapter);
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
}

View file

@ -15,6 +15,8 @@ public class CategoryPresenter extends BasePresenter<CategoryFragment> {
@Inject DatabaseHelper db; @Inject DatabaseHelper db;
private List<Category> categories;
private static final int GET_CATEGORIES = 1; private static final int GET_CATEGORIES = 1;
@Override @Override
@ -23,6 +25,7 @@ public class CategoryPresenter extends BasePresenter<CategoryFragment> {
restartableLatestCache(GET_CATEGORIES, restartableLatestCache(GET_CATEGORIES,
() -> db.getCategories().createObservable() () -> db.getCategories().createObservable()
.doOnNext(categories -> this.categories = categories)
.observeOn(AndroidSchedulers.mainThread()), .observeOn(AndroidSchedulers.mainThread()),
CategoryFragment::setCategories); CategoryFragment::setCategories);
@ -30,10 +33,36 @@ public class CategoryPresenter extends BasePresenter<CategoryFragment> {
} }
public void createCategory(String name) { public void createCategory(String name) {
db.insertCategory(Category.create(name)).createObservable().subscribe(); Category cat = Category.create(name);
// Set the new item in the last position
int max = 0;
if (categories != null) {
for (Category cat2 : categories) {
if (cat2.order > max) {
max = cat2.order + 1;
}
}
}
cat.order = max;
db.insertCategory(cat).createObservable().subscribe();
} }
public void deleteCategories(List<Category> categories) { public void deleteCategories(List<Category> categories) {
db.deleteCategories(categories).createObservable().subscribe(); db.deleteCategories(categories).createObservable().subscribe();
} }
public void reorderCategories(List<Category> categories) {
for (int i = 0; i < categories.size(); i++) {
categories.get(i).order = i;
}
db.insertCategories(categories).createObservable().subscribe();
}
public void renameCategory(Category category, String name) {
category.name = name;
db.insertCategory(category).createObservable().subscribe();
}
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View file

@ -21,14 +21,26 @@
android:layout_marginRight="@dimen/margin_right" android:layout_marginRight="@dimen/margin_right"
android:layout_marginEnd="@dimen/margin_right"/> android:layout_marginEnd="@dimen/margin_right"/>
<ImageView
android:id="@+id/reorder"
android:layout_width="50dp"
android:layout_height="50dp"
android:layout_marginLeft="@dimen/margin_left"
android:layout_marginStart="@dimen/margin_left"
android:layout_marginRight="@dimen/margin_right"
android:layout_marginEnd="@dimen/margin_right"
android:scaleType="center"
android:layout_alignParentRight="true"
android:src="@drawable/ic_reorder_grey_600_24dp"/>
<TextView <TextView
android:id="@+id/title" android:id="@+id/title"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginRight="@dimen/margin_right"
android:layout_marginEnd="@dimen/margin_right"
android:layout_toRightOf="@id/image" android:layout_toRightOf="@id/image"
android:layout_toEndOf="@id/image" android:layout_toEndOf="@id/image"
android:layout_toLeftOf="@id/reorder"
android:layout_toStartOf="@id/reorder"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:ellipsize="end" android:ellipsize="end"
android:maxLines="1" android:maxLines="1"

View file

@ -3,10 +3,14 @@
<menu xmlns:android="http://schemas.android.com/apk/res/android" <menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"> xmlns:app="http://schemas.android.com/apk/res-auto">
<item android:id="@+id/action_edit"
android:title="@string/action_edit"
android:icon="@drawable/ic_create"
app:showAsAction="ifRoom"/>
<item android:id="@+id/action_delete" <item android:id="@+id/action_delete"
android:title="@string/action_delete" android:title="@string/action_delete"
android:icon="@drawable/ic_action_delete" android:icon="@drawable/ic_action_delete"
android:orderInCategory="1" app:showAsAction="ifRoom"/>
app:showAsAction="always"/>
</menu> </menu>

View file

@ -24,6 +24,7 @@
<string name="action_edit">Edit</string> <string name="action_edit">Edit</string>
<string name="action_add_category">Add category</string> <string name="action_add_category">Add category</string>
<string name="action_edit_categories">Edit categories</string> <string name="action_edit_categories">Edit categories</string>
<string name="action_rename_category">Rename category</string>
<string name="action_sort_up">Sort up</string> <string name="action_sort_up">Sort up</string>
<string name="action_sort_down">Sort down</string> <string name="action_sort_down">Sort down</string>
<string name="action_show_unread">Unread</string> <string name="action_show_unread">Unread</string>