diff --git a/TODO.library b/TODO.library index 1808dc412..3250121a8 100644 --- a/TODO.library +++ b/TODO.library @@ -3,14 +3,16 @@ * Watch filesystem after loading DONE Covers loading in background DONE Wait messages -* Favorites +DONE Favorites +* Show text if the favorites list is empty DONE Search * Show wait message during search * File view -* Activity caption +DONE Activity caption * Book deleting * Show book info activity instead of immediate opening/menu * Main menu * Context menu * Reload book info from file * Reload book info for all the files +* Highlight current book diff --git a/assets/resources/application/en.xml b/assets/resources/application/en.xml index 685eb90ff..45a182e27 100644 --- a/assets/resources/application/en.xml +++ b/assets/resources/application/en.xml @@ -29,6 +29,8 @@ + + diff --git a/res/layout/library.xml b/res/layout/library.xml deleted file mode 100644 index 9bd3088c8..000000000 --- a/res/layout/library.xml +++ /dev/null @@ -1,26 +0,0 @@ - - - - - - - diff --git a/res/layout/library_ng_tree_item.xml b/res/layout/library_ng_tree_item.xml deleted file mode 100644 index f0a630e93..000000000 --- a/res/layout/library_ng_tree_item.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - - - - - - diff --git a/res/layout/library_tree_item.xml b/res/layout/library_tree_item.xml index 5ec288073..d8a841a7e 100644 --- a/res/layout/library_tree_item.xml +++ b/res/layout/library_tree_item.xml @@ -9,39 +9,43 @@ > - + - - - + + + + + diff --git a/src/org/geometerplus/android/fbreader/SQLiteBooksDatabase.java b/src/org/geometerplus/android/fbreader/SQLiteBooksDatabase.java index 430baab1f..1fb83c69f 100644 --- a/src/org/geometerplus/android/fbreader/SQLiteBooksDatabase.java +++ b/src/org/geometerplus/android/fbreader/SQLiteBooksDatabase.java @@ -60,7 +60,7 @@ public final class SQLiteBooksDatabase extends BooksDatabase { private void migrate(Context context) { final int version = myDatabase.getVersion(); - final int currentVersion = 10; + final int currentVersion = 11; if (version >= currentVersion) { return; } @@ -89,6 +89,8 @@ public final class SQLiteBooksDatabase extends BooksDatabase { updateTables8(); case 9: updateTables9(); + case 10: + updateTables10(); } myDatabase.setTransactionSuccessful(); myDatabase.endTransaction(); @@ -646,6 +648,40 @@ public final class SQLiteBooksDatabase extends BooksDatabase { return ids; } + private SQLiteStatement myAddToFavoritesStatement; + protected void addToFavorites(long bookId) { + if (myAddToFavoritesStatement == null) { + myAddToFavoritesStatement = myDatabase.compileStatement( + "INSERT OR IGNORE INTO Favorites(book_id) VALUES (?)" + ); + } + myAddToFavoritesStatement.bindLong(1, bookId); + myAddToFavoritesStatement.execute(); + } + + private SQLiteStatement myRemoveFromFavoritesStatement; + protected void removeFromFavorites(long bookId) { + if (myRemoveFromFavoritesStatement == null) { + myRemoveFromFavoritesStatement = myDatabase.compileStatement( + "DELETE FROM Favorites WHERE book_id = ?" + ); + } + myRemoveFromFavoritesStatement.bindLong(1, bookId); + myRemoveFromFavoritesStatement.execute(); + } + + protected List loadFavoritesIds() { + final Cursor cursor = myDatabase.rawQuery( + "SELECT book_id FROM Favorites", null + ); + final LinkedList ids = new LinkedList(); + while (cursor.moveToNext()) { + ids.add(cursor.getLong(0)); + } + cursor.close(); + return ids; + } + protected List loadBookmarks(long bookId) { LinkedList list = new LinkedList(); Cursor cursor = myDatabase.rawQuery( @@ -1071,4 +1107,10 @@ public final class SQLiteBooksDatabase extends BooksDatabase { private void updateTables9() { myDatabase.execSQL("CREATE INDEX BookList_BookIndex ON BookList (book_id)"); } + + private void updateTables10() { + myDatabase.execSQL( + "CREATE TABLE IF NOT EXISTS Favorites(" + + "book_id INTEGER UNIQUE NOT NULL REFERENCES Books(book_id))"); + } } diff --git a/src/org/geometerplus/android/fbreader/library/LibraryBaseActivity.java b/src/org/geometerplus/android/fbreader/library/LibraryBaseActivity.java index 1c1122cb5..b9e61c317 100644 --- a/src/org/geometerplus/android/fbreader/library/LibraryBaseActivity.java +++ b/src/org/geometerplus/android/fbreader/library/LibraryBaseActivity.java @@ -41,6 +41,7 @@ import org.geometerplus.zlibrary.ui.android.image.ZLAndroidImageLoader; import org.geometerplus.fbreader.tree.FBTree; import org.geometerplus.fbreader.library.*; +import org.geometerplus.android.fbreader.FBReader; import org.geometerplus.zlibrary.ui.android.R; @@ -48,12 +49,21 @@ import org.geometerplus.android.util.UIUtil; import org.geometerplus.android.fbreader.tree.ZLAndroidTree; abstract class LibraryBaseActivity extends ListActivity { + public static final String SELECTED_BOOK_PATH_KEY = "SelectedBookPath"; + static final String TREE_PATH_KEY = "TreePath"; + static final String PARAMETER_KEY = "Parameter"; + + static final String PATH_FAVORITES = "favorites"; + static final String PATH_SEARCH_RESULTS = "searchResults"; + static final String PATH_RECENT = "recent"; + static final String PATH_BY_AUTHOR = "byAuthor"; + static final String PATH_BY_TAG = "byTag"; + static Library Library; + static final ZLStringOption BookSearchPatternOption = new ZLStringOption("BookSearch", "Pattern", ""); - public static final String SELECTED_BOOK_PATH_KEY = "SelectedBookPath"; - protected final ZLResource myResource = ZLResource.resource("libraryView"); protected String mySelectedBookPath; @@ -64,11 +74,6 @@ abstract class LibraryBaseActivity extends ListActivity { mySelectedBookPath = getIntent().getStringExtra(SELECTED_BOOK_PATH_KEY); } - @Override - public void onListItemClick(ListView listView, View view, int position, long rowId) { - FBTree tree = ((LibraryAdapter)getListAdapter()).getItem(position); - } - @Override public boolean onSearchRequested() { startSearch(BookSearchPatternOption.getValue(), true, null, false); @@ -94,25 +99,45 @@ abstract class LibraryBaseActivity extends ListActivity { ).show(); } - protected final class LibraryAdapter extends BaseAdapter { + protected final class LibraryAdapter extends BaseAdapter implements View.OnCreateContextMenuListener { private final List myItems; public LibraryAdapter(List items) { myItems = items; } + @Override public final int getCount() { return myItems.size(); } + @Override public final FBTree getItem(int position) { return myItems.get(position); } + @Override public final long getItemId(int position) { return position; } + public void onCreateContextMenu(ContextMenu menu, View view, ContextMenu.ContextMenuInfo menuInfo) { + final int position = ((AdapterView.AdapterContextMenuInfo)menuInfo).position; + final LibraryTree tree = (LibraryTree)getItem(position); + if (tree instanceof BookTree) { + menu.setHeaderTitle(tree.getName()); + menu.add(0, OPEN_BOOK_ITEM_ID, 0, myResource.getResource("openBook").getValue()); + if (Library.isBookInFavorites(((BookTree)tree).Book)) { + menu.add(0, REMOVE_FROM_FAVORITES_ITEM_ID, 0, myResource.getResource("removeFromFavorites").getValue()); + } else { + menu.add(0, ADD_TO_FAVORITES_ITEM_ID, 0, myResource.getResource("addToFavorites").getValue()); + } + if ((Library.getRemoveBookMode(((BookTree)tree).Book) & Library.REMOVE_FROM_DISK) != 0) { + menu.add(0, DELETE_BOOK_ITEM_ID, 0, myResource.getResource("deleteBook").getValue()); + } + } + } + private int myCoverWidth = -1; private int myCoverHeight = -1; @@ -125,10 +150,10 @@ abstract class LibraryBaseActivity extends ListActivity { public View getView(int position, View convertView, final ViewGroup parent) { final FBTree tree = getItem(position); final View view = (convertView != null) ? convertView : - LayoutInflater.from(parent.getContext()).inflate(R.layout.library_ng_tree_item, parent, false); + LayoutInflater.from(parent.getContext()).inflate(R.layout.library_tree_item, parent, false); - ((TextView)view.findViewById(R.id.library_ng_tree_item_name)).setText(tree.getName()); - ((TextView)view.findViewById(R.id.library_ng_tree_item_childrenlist)).setText(tree.getSecondString()); + ((TextView)view.findViewById(R.id.library_tree_item_name)).setText(tree.getName()); + ((TextView)view.findViewById(R.id.library_tree_item_childrenlist)).setText(tree.getSecondString()); if (myCoverWidth == -1) { view.measure(ViewGroup.LayoutParams.FILL_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); @@ -137,7 +162,7 @@ abstract class LibraryBaseActivity extends ListActivity { view.requestLayout(); } - final ImageView coverView = (ImageView)view.findViewById(R.id.library_ng_tree_item_icon); + final ImageView coverView = (ImageView)view.findViewById(R.id.library_tree_item_icon); coverView.getLayoutParams().width = myCoverWidth; coverView.getLayoutParams().height = myCoverHeight; coverView.setScaleType(ImageView.ScaleType.CENTER_INSIDE); @@ -182,12 +207,57 @@ abstract class LibraryBaseActivity extends ListActivity { } } + protected void openBook(Book book) { + startActivity( + new Intent(getApplicationContext(), FBReader.class) + .setAction(Intent.ACTION_VIEW) + .putExtra(FBReader.BOOK_PATH_KEY, book.File.getPath()) + .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) + ); + } + + private static final int OPEN_BOOK_ITEM_ID = 0; + private static final int ADD_TO_FAVORITES_ITEM_ID = 1; + private static final int REMOVE_FROM_FAVORITES_ITEM_ID = 2; + private static final int DELETE_BOOK_ITEM_ID = 3; + + @Override + public boolean onContextItemSelected(MenuItem item) { + final int position = ((AdapterView.AdapterContextMenuInfo)item.getMenuInfo()).position; + final FBTree tree = ((LibraryAdapter)getListAdapter()).getItem(position); + if (tree instanceof BookTree) { + final BookTree bookTree = (BookTree)tree; + switch (item.getItemId()) { + case OPEN_BOOK_ITEM_ID: + openBook(bookTree.Book); + return true; + case ADD_TO_FAVORITES_ITEM_ID: + Library.addBookToFavorites(bookTree.Book); + return true; + case REMOVE_FROM_FAVORITES_ITEM_ID: + Library.removeBookFromFavorites(bookTree.Book); + getListView().invalidateViews(); + return true; + case DELETE_BOOK_ITEM_ID: + // TODO: implement + return true; + } + } + return super.onContextItemSelected(item); + } + protected class OpenTreeRunnable implements Runnable { private final String myTreePath; + private final String myParameter; private final String mySelectedBookPath; public OpenTreeRunnable(String treePath, String selectedBookPath) { + this(treePath, null, selectedBookPath); + } + + public OpenTreeRunnable(String treePath, String parameter, String selectedBookPath) { myTreePath = treePath; + myParameter = parameter; mySelectedBookPath = selectedBookPath; } @@ -197,7 +267,8 @@ abstract class LibraryBaseActivity extends ListActivity { startActivity( new Intent(LibraryBaseActivity.this, LibraryTreeActivity.class) .putExtra(SELECTED_BOOK_PATH_KEY, mySelectedBookPath) - .putExtra(LibraryTreeActivity.TREE_PATH_KEY, myTreePath) + .putExtra(TREE_PATH_KEY, myTreePath) + .putExtra(PARAMETER_KEY, myParameter) ); } }; diff --git a/src/org/geometerplus/android/fbreader/library/LibraryTopLevelActivity.java b/src/org/geometerplus/android/fbreader/library/LibraryTopLevelActivity.java index fb1a956f4..fab34968e 100644 --- a/src/org/geometerplus/android/fbreader/library/LibraryTopLevelActivity.java +++ b/src/org/geometerplus/android/fbreader/library/LibraryTopLevelActivity.java @@ -60,24 +60,24 @@ public class LibraryTopLevelActivity extends LibraryBaseActivity { myItems = new LinkedList(); myItems.add(new TopLevelTree( - myResource.getResource("favorites"), + myResource.getResource(PATH_FAVORITES), R.drawable.ic_list_library_favorites, - new OpenTreeRunnable(LibraryTreeActivity.PATH_FAVORITES, mySelectedBookPath) + new OpenTreeRunnable(PATH_FAVORITES, mySelectedBookPath) )); myItems.add(new TopLevelTree( - myResource.getResource("recent"), + myResource.getResource(PATH_RECENT), R.drawable.ic_list_library_recent, - new OpenTreeRunnable(LibraryTreeActivity.PATH_RECENT, mySelectedBookPath) + new OpenTreeRunnable(PATH_RECENT, mySelectedBookPath) )); myItems.add(new TopLevelTree( - myResource.getResource("byAuthor"), + myResource.getResource(PATH_BY_AUTHOR), R.drawable.ic_list_library_authors, - new OpenTreeRunnable(LibraryTreeActivity.PATH_BY_AUTHOR, mySelectedBookPath) + new OpenTreeRunnable(PATH_BY_AUTHOR, mySelectedBookPath) )); myItems.add(new TopLevelTree( - myResource.getResource("byTag"), + myResource.getResource(PATH_BY_TAG), R.drawable.ic_list_library_tags, - new OpenTreeRunnable(LibraryTreeActivity.PATH_BY_TAG, mySelectedBookPath) + new OpenTreeRunnable(PATH_BY_TAG, mySelectedBookPath) )); myItems.add(new TopLevelTree( myResource.getResource("fileTree"), @@ -109,11 +109,12 @@ public class LibraryTopLevelActivity extends LibraryBaseActivity { if (myItems.get(0) == mySearchResultsItem) { myItems.remove(0); } + final String pattern = intent.getStringExtra(SearchManager.QUERY); mySearchResultsItem = new TopLevelTree( - myResource.getResource("searchResults"), - intent.getStringExtra(SearchManager.QUERY), + myResource.getResource(PATH_SEARCH_RESULTS), + pattern, R.drawable.ic_list_library_books, - new OpenTreeRunnable(LibraryTreeActivity.PATH_SEARCH_RESULTS, mySelectedBookPath) + new OpenTreeRunnable(PATH_SEARCH_RESULTS, pattern, mySelectedBookPath) ); myItems.add(0, mySearchResultsItem); getListView().invalidateViews(); diff --git a/src/org/geometerplus/android/fbreader/library/LibraryTreeActivity.java b/src/org/geometerplus/android/fbreader/library/LibraryTreeActivity.java index a3ddbaf43..c8615f671 100644 --- a/src/org/geometerplus/android/fbreader/library/LibraryTreeActivity.java +++ b/src/org/geometerplus/android/fbreader/library/LibraryTreeActivity.java @@ -29,17 +29,7 @@ import org.geometerplus.fbreader.tree.FBTree; import org.geometerplus.fbreader.library.Library; import org.geometerplus.fbreader.library.BookTree; -import org.geometerplus.android.fbreader.FBReader; - public class LibraryTreeActivity extends LibraryBaseActivity { - static final String TREE_PATH_KEY = "TreePath"; - - static final String PATH_FAVORITES = "favorites"; - static final String PATH_SEARCH_RESULTS = "searchResults"; - static final String PATH_RECENT = "recent"; - static final String PATH_BY_AUTHOR = "author"; - static final String PATH_BY_TAG = "tag"; - private String myTreePathString; private String mySelectedBookPath; @@ -59,34 +49,50 @@ public class LibraryTreeActivity extends LibraryBaseActivity { showNotFoundToast(); finish(); } + return; + } + + myTreePathString = intent.getStringExtra(TREE_PATH_KEY); + mySelectedBookPath = intent.getStringExtra(SELECTED_BOOK_PATH_KEY); + + final String[] path = myTreePathString.split("\000"); + + String title = null; + if (path.length == 1) { + title = myResource.getResource(path[0]).getResource("summary").getValue(); + final String parameter = intent.getStringExtra(PARAMETER_KEY); + if (parameter != null) { + title = title.replace("%s", parameter); + } } else { - myTreePathString = getIntent().getStringExtra(TREE_PATH_KEY); - mySelectedBookPath = getIntent().getStringExtra(SELECTED_BOOK_PATH_KEY); + title = path[path.length - 1]; + } + setTitle(title); + + FBTree tree = null; + if (PATH_RECENT.equals(path[0])) { + tree = Library.recentBooks(); + } else if (PATH_SEARCH_RESULTS.equals(path[0])) { + tree = Library.searchResults(); + } else if (PATH_BY_AUTHOR.equals(path[0])) { + tree = Library.byAuthor(); + } else if (PATH_BY_TAG.equals(path[0])) { + tree = Library.byTag(); + } else if (PATH_FAVORITES.equals(path[0])) { + tree = Library.favorites(); + } - final String[] path = myTreePathString.split("\000"); - - FBTree tree = null; - if (PATH_RECENT.equals(path[0])) { - tree = Library.recentBooks(); - } else if (PATH_SEARCH_RESULTS.equals(path[0])) { - tree = Library.searchResults(); - } else if (PATH_BY_AUTHOR.equals(path[0])) { - tree = Library.byAuthor(); - } else if (PATH_BY_TAG.equals(path[0])) { - tree = Library.byTag(); - } else if (PATH_FAVORITES.equals(path[0])) { + for (int i = 1; i < path.length; ++i) { + if (tree == null) { + break; } + tree = tree.getSubTreeByName(path[i]); + } - for (int i = 1; i < path.length; ++i) { - if (tree == null) { - break; - } - tree = tree.getSubTreeByName(path[i]); - } - - if (tree != null) { - setListAdapter(new LibraryAdapter(tree.subTrees())); - } + if (tree != null) { + final LibraryAdapter adapter = new LibraryAdapter(tree.subTrees()); + setListAdapter(adapter); + getListView().setOnCreateContextMenuListener(adapter); } } @@ -94,12 +100,7 @@ public class LibraryTreeActivity extends LibraryBaseActivity { public void onListItemClick(ListView listView, View view, int position, long rowId) { FBTree tree = ((LibraryAdapter)getListAdapter()).getItem(position); if (tree instanceof BookTree) { - startActivity( - new Intent(getApplicationContext(), FBReader.class) - .setAction(Intent.ACTION_VIEW) - .putExtra(FBReader.BOOK_PATH_KEY, ((BookTree)tree).Book.File.getPath()) - .addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_NEW_TASK) - ); + openBook(((BookTree)tree).Book); } else { new OpenTreeRunnable( myTreePathString + "\000" + tree.getName(), diff --git a/src/org/geometerplus/fbreader/library/BooksDatabase.java b/src/org/geometerplus/fbreader/library/BooksDatabase.java index e4259a9ba..08e8d71df 100644 --- a/src/org/geometerplus/fbreader/library/BooksDatabase.java +++ b/src/org/geometerplus/fbreader/library/BooksDatabase.java @@ -84,6 +84,10 @@ public abstract class BooksDatabase { protected abstract List loadRecentBookIds(); protected abstract void saveRecentBookIds(final List ids); + protected abstract List loadFavoritesIds(); + protected abstract void addToFavorites(long bookId); + protected abstract void removeFromFavorites(long bookId); + protected Bookmark createBookmark(long id, long bookId, String bookTitle, String text, Date creationDate, Date modificationDate, Date accessDate, int accessCounter, String modelId, int paragraphIndex, int wordIndex, int charIndex) { return new Bookmark(id, bookId, bookTitle, text, creationDate, modificationDate, accessDate, accessCounter, modelId, paragraphIndex, wordIndex, charIndex); } diff --git a/src/org/geometerplus/fbreader/library/Library.java b/src/org/geometerplus/fbreader/library/Library.java index d179f8487..279f303ce 100644 --- a/src/org/geometerplus/fbreader/library/Library.java +++ b/src/org/geometerplus/fbreader/library/Library.java @@ -36,6 +36,7 @@ public final class Library { private final LibraryTree myLibraryByAuthor = new RootTree(); private final LibraryTree myLibraryByTag = new RootTree(); private final LibraryTree myRecentBooks = new RootTree(); + private final LibraryTree myFavorites = new RootTree(); private final LibraryTree mySearchResult = new RootTree(); private volatile int myState = STATE_NOT_INITIALIZED; @@ -268,6 +269,14 @@ public final class Library { } } + for (long id : db.loadFavoritesIds()) { + Book book = bookById.get(id); + if (book != null) { + myFavorites.createBookSubTree(book, true); + } + } + myFavorites.sortAllChildren(); + db.executeAsATransaction(new Runnable() { public void run() { for (Book book : myBooks) { @@ -309,6 +318,11 @@ public final class Library { return (recentIds.size() > 0) ? Book.getById(recentIds.get(0)) : null; } + public LibraryTree favorites() { + waitForState(STATE_FULLY_INITIALIZED); + return myFavorites; + } + public LibraryTree searchResults() { return mySearchResult; } @@ -340,6 +354,27 @@ public final class Library { db.saveRecentBookIds(ids); } + public boolean isBookInFavorites(Book book) { + waitForState(STATE_FULLY_INITIALIZED); + return myFavorites.containsBook(book); + } + + public void addBookToFavorites(Book book) { + waitForState(STATE_FULLY_INITIALIZED); + if (!myFavorites.containsBook(book)) { + myFavorites.createBookSubTree(book, true); + myFavorites.sortAllChildren(); + BooksDatabase.Instance().addToFavorites(book.getId()); + } + } + + public void removeBookFromFavorites(Book book) { + waitForState(STATE_FULLY_INITIALIZED); + if (myFavorites.removeBook(book)) { + BooksDatabase.Instance().removeFromFavorites(book.getId()); + } + } + public static final int REMOVE_DONT_REMOVE = 0x00; public static final int REMOVE_FROM_LIBRARY = 0x01; public static final int REMOVE_FROM_DISK = 0x02; @@ -380,6 +415,7 @@ public final class Library { db.saveRecentBookIds(ids); } mySearchResult.removeBook(book); + myFavorites.removeBook(book); BooksDatabase.Instance().deleteFromBookList(book.getId()); if ((removeMode & REMOVE_FROM_DISK) != 0) { diff --git a/src/org/geometerplus/fbreader/library/LibraryTree.java b/src/org/geometerplus/fbreader/library/LibraryTree.java index 299b25b07..1238f1e6e 100644 --- a/src/org/geometerplus/fbreader/library/LibraryTree.java +++ b/src/org/geometerplus/fbreader/library/LibraryTree.java @@ -46,6 +46,15 @@ public abstract class LibraryTree extends FBTree { return new BookTree(this, book, showAuthors); } + public boolean containsBook(Book book) { + for (FBTree tree : this) { + if ((tree instanceof BookTree) && ((BookTree)tree).Book.equals(book)) { + return true; + } + } + return false; + } + public boolean removeBook(Book book) { final LinkedList toRemove = new LinkedList(); for (FBTree tree : this) {