├── .gitignore ├── LICENSE ├── README.md ├── app ├── .gitignore ├── app-release.apk ├── build.gradle ├── proguard-rules.pro └── src │ └── main │ ├── AndroidManifest.xml │ ├── assets │ └── licenses.html │ ├── java │ ├── moe │ │ └── feng │ │ │ └── nhentai │ │ │ ├── api │ │ │ ├── BookApi.java │ │ │ ├── PageApi.java │ │ │ └── common │ │ │ │ └── NHentaiUrl.java │ │ │ ├── cache │ │ │ ├── common │ │ │ │ └── Constants.java │ │ │ └── file │ │ │ │ ├── FileCacheManager.java │ │ │ │ └── OfflineDocumentManager.java │ │ │ ├── dao │ │ │ ├── CommonPreferences.java │ │ │ └── SearchHistoryManager.java │ │ │ ├── model │ │ │ ├── BaseMessage.java │ │ │ └── Book.java │ │ │ ├── ui │ │ │ ├── BookDetailsActivity.java │ │ │ ├── CategoryActivity.java │ │ │ ├── GalleryActivity.java │ │ │ ├── MainActivity.java │ │ │ ├── SearchResultActivity.java │ │ │ ├── SettingsActivity.java │ │ │ ├── adapter │ │ │ │ ├── BookListRecyclerAdapter.java │ │ │ │ ├── BookPreviewGridAdapter.java │ │ │ │ ├── GalleryPagerAdapter.java │ │ │ │ └── HomePagerAdapter.java │ │ │ ├── common │ │ │ │ ├── AbsActivity.java │ │ │ │ └── AbsRecyclerViewAdapter.java │ │ │ └── fragment │ │ │ │ ├── BookPageFragment.java │ │ │ │ ├── main │ │ │ │ ├── DownloadManagerFragment.java │ │ │ │ ├── FavoriteFragment.java │ │ │ │ └── HomeFragment.java │ │ │ │ └── settings │ │ │ │ ├── SettingsLicense.java │ │ │ │ └── SettingsMain.java │ │ │ ├── util │ │ │ ├── AsyncTask.java │ │ │ ├── ColorGenerator.java │ │ │ ├── FullScreenHelper.java │ │ │ ├── HttpTools.java │ │ │ ├── Settings.java │ │ │ ├── TextDrawable.java │ │ │ └── Utility.java │ │ │ └── view │ │ │ ├── AutoWrapLayout.java │ │ │ ├── ExpandableHeightGridView.java │ │ │ ├── WheelProgressView.java │ │ │ └── pref │ │ │ ├── Preference.java │ │ │ ├── SwitchPreference.java │ │ │ └── TwoStatePreference.java │ └── sumimakito │ │ └── android │ │ └── quickkv │ │ ├── DataProcessor.java │ │ ├── QKVConfig.java │ │ ├── QKVFSReader.java │ │ ├── QKVLogger.java │ │ ├── QuickKV.java │ │ ├── database │ │ ├── KeyValueDatabase.java │ │ └── QKVDatabase.java │ │ └── security │ │ └── AES256.java │ └── res │ ├── color │ └── drawer_item_color.xml │ ├── drawable-hdpi │ ├── ic_face_unlock_black_24dp.png │ ├── ic_history.png │ ├── ic_home_black_24dp.png │ ├── ic_photo_library_black_24dp.png │ └── ic_style_black_24dp.png │ ├── drawable-xhdpi │ ├── ic_face_unlock_black_24dp.png │ ├── ic_history.png │ ├── ic_home_black_24dp.png │ ├── ic_photo_library_black_24dp.png │ └── ic_style_black_24dp.png │ ├── drawable-xxhdpi │ ├── ic_face_unlock_black_24dp.png │ ├── ic_history.png │ ├── ic_home_black_24dp.png │ ├── ic_photo_library_black_24dp.png │ └── ic_style_black_24dp.png │ ├── drawable-xxxhdpi │ └── ic_photo_library_black_24dp.png │ ├── drawable │ ├── holder_0.jpg │ ├── holder_1.png │ ├── holder_2.jpg │ ├── shadow_gradient.xml │ ├── shadow_gradient_reserve.xml │ ├── shadow_normal.xml │ └── shadow_normal_reserve.xml │ ├── layout │ ├── activity_book_details.xml │ ├── activity_gallery.xml │ ├── activity_main.xml │ ├── activity_search_result.xml │ ├── activity_settings.xml │ ├── custom_preference.xml │ ├── custom_preference_widget_switch.xml │ ├── fragment_book_page.xml │ ├── fragment_download.xml │ ├── fragment_favorite.xml │ ├── fragment_home.xml │ ├── list_item_book_card.xml │ ├── list_item_book_picture_thumb.xml │ ├── list_item_menu_row.xml │ └── navigation_header.xml │ ├── menu │ ├── menu_main.xml │ └── navigation_menu.xml │ ├── mipmap-hdpi │ └── ic_launcher.png │ ├── mipmap-xhdpi │ └── ic_launcher.png │ ├── mipmap-xxhdpi │ └── ic_launcher.png │ ├── values-v21 │ └── styles.xml │ ├── values-zh-rCN │ └── strings.xml │ ├── values-zh-rTW │ └── strings.xml │ ├── values │ ├── attrs.xml │ ├── color.xml │ ├── dimen.xml │ ├── strings.xml │ └── styles.xml │ └── xml │ └── settings_main.xml ├── art ├── nhbooks.png ├── screenshot0.png └── screenshot1.png ├── build.gradle ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── libraries └── PersistentSearch │ ├── .gitignore │ ├── build.gradle │ ├── gradle.properties │ ├── proguard-rules.pro │ └── src │ └── main │ ├── AndroidManifest.xml │ ├── java │ ├── com │ │ ├── balysv │ │ │ └── materialmenu │ │ │ │ ├── MaterialMenu.java │ │ │ │ ├── MaterialMenuDrawable.java │ │ │ │ └── MaterialMenuView.java │ │ └── quinny898 │ │ │ └── library │ │ │ └── persistentsearch │ │ │ ├── SearchBox.java │ │ │ └── SearchResult.java │ └── io │ │ └── codetail │ │ ├── animation │ │ ├── RevealAnimator.java │ │ ├── ReverseInterpolator.java │ │ ├── SupportAnimator.java │ │ ├── SupportAnimatorLollipop.java │ │ ├── SupportAnimatorPreL.java │ │ └── ViewAnimationUtils.java │ │ └── widget │ │ ├── RevealFrameLayout.java │ │ └── RevealLinearLayout.java │ └── res │ ├── anim │ └── anim_down.xml │ ├── drawable-hdpi │ ├── ic_action_mic.png │ ├── ic_clear.png │ ├── ic_up.png │ └── search_bg.9.png │ ├── drawable-mdpi │ ├── ic_action_mic.png │ ├── ic_clear.png │ ├── ic_up.png │ └── search_bg.9.png │ ├── drawable-xhdpi │ ├── ic_action_mic.png │ ├── ic_clear.png │ ├── ic_up.png │ └── search_bg.9.png │ ├── drawable-xxhdpi │ ├── ic_action_mic.png │ ├── ic_clear.png │ ├── ic_up.png │ ├── search_bg.9.png │ ├── search_bg_shadow.9.png │ ├── search_bg_transparent.9.png │ └── search_frame.9.png │ ├── layout │ ├── search_option.xml │ └── searchbox.xml │ └── values │ ├── strings.xml │ └── styles.xml └── settings.gradle /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | /local.properties 3 | /.idea 4 | .DS_Store 5 | /build 6 | 7 | *.iml 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NHBooks for Android 2 | ![NHBooksLogo](/art/nhbooks.png) 3 | 4 | a Material Design NHentai client for Android. 5 | 一枚 Material Design 風格的 NHentai Android 客戶端 6 | 7 | ### 螢幕截圖 8 | ![S0](/art/screenshot0.png) 9 | 10 | ![S1](/art/screenshot1.png) 11 | 12 | ## 軟體說明 13 | 該程式按照 Material Design 設計規範,提供簡潔、美觀的介面,並通過 API 從 NHentai 獲取本子,給你一個輕量、方便的客戶端。 14 | 15 | ### 特別聲明 16 | 該應用程式所供應的內容不適合未成年人觀看,所有內容通過 Jsoup 解析 NHentai 官網獲得,內容有任何異議或造成心理甚至生理上的問題均與本項目無關。 17 | 18 | 觀看時請留意是否適用於當地法律法規。 19 | 20 | ### 聯絡我 21 | 22 | Google Plus: +Fung Jichun 23 | 24 | 新浪微博: @某燒餅 25 | 26 | ### 支持項目 27 | 28 | Alipay 支付寶: 316643843@qq.com 29 | 30 | ### License 開源協議 31 | 32 | ``` 33 | GNU GENERAL PUBLIC LICENSE Version 3, 29 June 2007 34 | 35 | Copyright (C) 2015 FengMoe Team 36 | 37 | This program comes with ABSOLUTELY NO WARRANTY. 38 | This is free software, and you are welcome to redistribute it under certain conditions. 39 | ``` 40 | -------------------------------------------------------------------------------- /app/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /app/app-release.apk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/app-release.apk -------------------------------------------------------------------------------- /app/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.application' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | applicationId "moe.feng.nhentai" 9 | minSdkVersion 15 10 | targetSdkVersion 22 11 | versionCode 1 12 | versionName "1.0" 13 | } 14 | buildTypes { 15 | release { 16 | minifyEnabled false 17 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 18 | } 19 | } 20 | } 21 | 22 | dependencies { 23 | compile fileTree(dir: 'libs', include: ['*.jar']) 24 | compile project(':libraries:PersistentSearch') 25 | compile 'com.android.support:support-v13:22.2.0' 26 | compile 'com.android.support:design:22.2.0' 27 | compile 'com.android.support:appcompat-v7:22.2.0' 28 | compile 'com.android.support:cardview-v7:22.2.0' 29 | compile 'com.android.support:recyclerview-v7:22.2.0' 30 | compile 'com.google.code.gson:gson:2.3.1' 31 | compile 'org.jsoup:jsoup:1.8.2' 32 | compile 'com.github.chrisbanes.photoview:library:1.2.3' 33 | compile 'com.squareup.picasso:picasso:2.5.2' 34 | compile ('com.github.florent37:materialimageloading:1.0.1@aar'){ 35 | transitive = true 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in E:\Feng\sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 13 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 26 | 27 | 28 | 29 | 30 | 31 | 35 | 36 | 37 | 38 | 39 | 40 | 44 | 45 | 46 | 47 | 48 | 49 | 53 | 54 | 55 | 56 | 57 | 58 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/api/PageApi.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.api; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.util.Log; 6 | 7 | import org.jsoup.Jsoup; 8 | import org.jsoup.nodes.Document; 9 | import org.jsoup.nodes.Element; 10 | import org.jsoup.select.Elements; 11 | 12 | import java.io.File; 13 | import java.io.IOException; 14 | import java.util.ArrayList; 15 | 16 | import moe.feng.nhentai.api.common.NHentaiUrl; 17 | import moe.feng.nhentai.cache.file.FileCacheManager; 18 | import moe.feng.nhentai.model.BaseMessage; 19 | import moe.feng.nhentai.model.Book; 20 | 21 | import static moe.feng.nhentai.cache.common.Constants.CACHE_PAGE_IMG; 22 | 23 | public class PageApi { 24 | 25 | public static final String TAG = PageApi.class.getSimpleName(); 26 | 27 | public static BaseMessage getPageList(String url) { 28 | BaseMessage result = new BaseMessage(); 29 | 30 | Document doc; 31 | try { 32 | doc = Jsoup.connect(url).get(); 33 | } catch (IOException e) { 34 | result.setCode(403); 35 | e.printStackTrace(); 36 | return result; 37 | } 38 | 39 | Elements container = doc.getElementsByClass("outer-preview-container"); 40 | 41 | ArrayList books = new ArrayList<>(); 42 | 43 | for (Element e : container) { 44 | Book book = new Book(); 45 | 46 | Element caption = e.getElementsByClass("caption").get(0); 47 | Element titleElement = caption.getElementsByTag("a").get(0); 48 | String bookId = titleElement.attr("href"); 49 | bookId = bookId.substring(0, bookId.lastIndexOf("/")); 50 | bookId = bookId.substring(bookId.lastIndexOf("/") + 1, bookId.length()); 51 | book.bookId = bookId; 52 | book.title = titleElement.text(); 53 | 54 | Elements imgs = e.getElementsByTag("img"); 55 | for (Element imge : imgs) { 56 | if (imge.hasAttr("src")) { 57 | String thumbUrl = imge.attr("src"); 58 | thumbUrl = thumbUrl.substring(0, thumbUrl.lastIndexOf("/")); 59 | String galleryId = thumbUrl.substring(thumbUrl.lastIndexOf("/") + 1, thumbUrl.length()); 60 | book.galleryId = galleryId; 61 | book.bigCoverImageUrl = NHentaiUrl.getBigCoverUrl(galleryId); 62 | book.previewImageUrl = NHentaiUrl.getThumbUrl(galleryId); 63 | try { 64 | book.thumbHeight = Integer.valueOf(imge.attr("height")); 65 | book.thumbWidth = Integer.valueOf(imge.attr("width")); 66 | } catch (Exception ex) { 67 | 68 | } 69 | } 70 | } 71 | 72 | if (book.bookId != null && !book.bookId.isEmpty()) { 73 | books.add(book); 74 | } 75 | 76 | Log.i(TAG, "Get book: " + book.toJSONString()); 77 | } 78 | 79 | result.setCode(0); 80 | result.setData(books); 81 | 82 | return result; 83 | } 84 | 85 | public static BaseMessage getHomePageList(int number) { 86 | return getPageList(NHentaiUrl.getHomePageUrl(number)); 87 | } 88 | 89 | public static BaseMessage getSearchPageList(String keyword, int number) { 90 | return getPageList(NHentaiUrl.getSearchUrl(keyword, number)); 91 | } 92 | 93 | public static Bitmap getPageOriginImage(Context context, Book book, int page_num) { 94 | String url = NHentaiUrl.getOriginPictureUrl(book.galleryId, String.valueOf(page_num)); 95 | FileCacheManager m = FileCacheManager.getInstance(context); 96 | 97 | if (!m.cacheExistsUrl(CACHE_PAGE_IMG, url) && !m.createCacheFromNetwork(CACHE_PAGE_IMG, url)) { 98 | return null; 99 | } 100 | 101 | return m.getBitmapUrl(CACHE_PAGE_IMG, url); 102 | } 103 | 104 | public static File getPageOriginImageFile(Context context, Book book, int page_num) { 105 | String url = NHentaiUrl.getOriginPictureUrl(book.galleryId, String.valueOf(page_num)); 106 | FileCacheManager m = FileCacheManager.getInstance(context); 107 | 108 | if (!m.cacheExistsUrl(CACHE_PAGE_IMG, url) && !m.createCacheFromNetwork(CACHE_PAGE_IMG, url)) { 109 | return null; 110 | } 111 | 112 | return m.getBitmapUrlFile(CACHE_PAGE_IMG, url); 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/api/common/NHentaiUrl.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.api.common; 2 | 3 | public class NHentaiUrl { 4 | 5 | public static final String NHENTAI_HOME = "http://nhentai.net"; 6 | public static final String NHENTAI_I = "http://i.nhentai.net"; 7 | 8 | public static String getSearchUrl(String content) { 9 | return getSearchUrl(content, 1); 10 | } 11 | 12 | public static String getSearchUrl(String content, int page_num) { 13 | String targetContent = content; 14 | if (targetContent.contains(" ")) { 15 | targetContent = targetContent.replaceAll(" ", "+"); 16 | } 17 | return NHENTAI_HOME + "/search/?q=" + targetContent + "&page=" + page_num; 18 | } 19 | 20 | public static String getBookDetailsUrl(String book_id) { 21 | return NHENTAI_HOME + "/g/" + book_id; 22 | } 23 | 24 | public static String getBookPageUrl(String book_id, int page_num) { 25 | return getBookDetailsUrl(book_id) + "/" + page_num; 26 | } 27 | 28 | public static String getGalleryUrl(String g_id) { 29 | return NHENTAI_I + "/galleries/" + g_id; 30 | } 31 | 32 | public static String getOriginPictureUrl(String g_id, String page_num) { 33 | return getPictureUrl(g_id, page_num, "jpg"); 34 | } 35 | 36 | public static String getThumbPictureUrl(String g_id, String page_num) { 37 | return getPictureUrl(g_id, page_num + "t", "jpg"); 38 | } 39 | 40 | public static String getThumbUrl(String g_id) { 41 | return getPictureUrl(g_id, "thumb", "jpg"); 42 | } 43 | 44 | public static String getBigCoverUrl(String g_id) { 45 | // TODO Not all covers are jpgs 46 | return getPictureUrl(g_id, "cover", "jpg"); 47 | } 48 | 49 | public static String getPictureUrl(String g_id, String page_num, String file_type) { 50 | return getGalleryUrl(g_id) + "/" + page_num + "." + file_type; 51 | } 52 | 53 | public static String getParodyUrl(String name) { 54 | String targetName = name; 55 | if (targetName.contains(" ")) { 56 | targetName = targetName.replaceAll(" ", "-"); 57 | } 58 | return NHENTAI_HOME + "/parody/" + targetName; 59 | } 60 | 61 | public static String getCharacterUrl(String name) { 62 | String targetName = name; 63 | if (targetName.contains(" ")) { 64 | targetName = targetName.replaceAll(" ", "-"); 65 | } 66 | return NHENTAI_HOME + "/character/" + targetName; 67 | } 68 | 69 | public static String getTagUrl(String tag) { 70 | String targetTag = tag; 71 | if (targetTag.contains(" ")) { 72 | targetTag = targetTag.replaceAll(" ", "-"); 73 | } 74 | return NHENTAI_HOME + "/tagged/" + targetTag; 75 | } 76 | 77 | public static String getArtistUrl(String name) { 78 | String targetName = name; 79 | if (targetName.contains(" ")) { 80 | targetName = targetName.replaceAll(" ", "-"); 81 | } 82 | return NHENTAI_HOME + "/artist/" + targetName; 83 | } 84 | 85 | public static String getGroupUrl(String name) { 86 | String targetName = name; 87 | if (targetName.contains(" ")) { 88 | targetName = targetName.replaceAll(" ", "-"); 89 | } 90 | return NHENTAI_HOME + "/group/" + targetName; 91 | } 92 | 93 | public static String getLanguageUrl(String name) { 94 | String targetName = name; 95 | if (targetName.contains(" ")) { 96 | targetName = targetName.replaceAll(" ", "-"); 97 | } 98 | return NHENTAI_HOME + "/language/" + targetName; 99 | } 100 | 101 | public static String getHomePageUrl(int page) { 102 | return NHENTAI_HOME + "/?page=" + page; 103 | } 104 | 105 | } 106 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/cache/common/Constants.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.cache.common; 2 | 3 | public class Constants { 4 | 5 | public static final String CACHE_COVER = "cover", CACHE_PAGE_IMG = "pages", 6 | CACHE_PAGE_THUMB = "thumb", CACHE_THUMB = "thumb", CACHE_DOCUMENT = "document"; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/cache/file/FileCacheManager.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.cache.file; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.graphics.BitmapFactory; 6 | import android.util.Log; 7 | 8 | import java.io.File; 9 | import java.io.FileNotFoundException; 10 | import java.io.FileInputStream; 11 | import java.io.FileOutputStream; 12 | import java.io.InputStream; 13 | import java.io.IOException; 14 | import java.net.URL; 15 | import java.net.HttpURLConnection; 16 | import java.net.MalformedURLException; 17 | 18 | import static moe.feng.nhentai.BuildConfig.DEBUG; 19 | 20 | public class FileCacheManager { 21 | 22 | private static final String TAG = FileCacheManager.class.getSimpleName(); 23 | 24 | private static FileCacheManager sInstance; 25 | 26 | private File mCacheDir; 27 | 28 | public static final FileCacheManager getInstance(Context context) { 29 | if (sInstance == null) { 30 | sInstance = new FileCacheManager(context); 31 | } 32 | 33 | return sInstance; 34 | } 35 | 36 | private FileCacheManager(Context context) { 37 | mCacheDir = context.getExternalCacheDir(); 38 | } 39 | 40 | public boolean createCacheFromNetwork(String type, String url) { 41 | 42 | if (DEBUG) { 43 | Log.d(TAG, "requesting cache from " + url); 44 | } 45 | 46 | URL u; 47 | 48 | try { 49 | u = new URL(url); 50 | } catch (MalformedURLException e) { 51 | return false; 52 | } 53 | 54 | HttpURLConnection conn; 55 | 56 | try { 57 | conn = (HttpURLConnection) u.openConnection(); 58 | } catch (IOException e) { 59 | return false; 60 | } 61 | 62 | conn.setConnectTimeout(5000); 63 | 64 | try { 65 | if (conn.getResponseCode() != 200) { 66 | if (url.contains("jpg")) { 67 | try { 68 | u = new URL(url.replace("jpg", "png")); 69 | } catch (MalformedURLException ex) { 70 | return false; 71 | } 72 | try { 73 | conn = (HttpURLConnection) u.openConnection(); 74 | } catch (IOException ex) { 75 | return false; 76 | } 77 | } else { 78 | return false; 79 | } 80 | } 81 | } catch (IOException e) { 82 | e.printStackTrace(); 83 | } 84 | 85 | try { 86 | return createCacheFromStrem(type, getCacheName(url), conn.getInputStream()); 87 | } catch (IOException e) { 88 | return false; 89 | } 90 | } 91 | 92 | public boolean createCacheFromStrem(String type, String name, InputStream stream) { 93 | File f = new File(getCachePath(type, name) + "_downloading"); 94 | f.getParentFile().mkdirs(); 95 | f.getParentFile().mkdir(); 96 | 97 | if (f.exists()) { 98 | f.delete(); 99 | } 100 | 101 | try { 102 | f.createNewFile(); 103 | } catch (IOException e) { 104 | return false; 105 | } 106 | 107 | FileOutputStream opt; 108 | 109 | try { 110 | opt = new FileOutputStream(f); 111 | } catch (FileNotFoundException e) { 112 | return false; 113 | } 114 | 115 | byte[] buf = new byte[512]; 116 | int len = 0; 117 | 118 | try { 119 | while ((len = stream.read(buf)) != -1) { 120 | opt.write(buf, 0, len); 121 | } 122 | } catch (IOException e) { 123 | return false; 124 | } 125 | 126 | try { 127 | stream.close(); 128 | opt.close(); 129 | } catch (IOException e) { 130 | 131 | } 132 | 133 | f.renameTo(new File(getCachePath(type, name))); 134 | 135 | return true; 136 | } 137 | 138 | // True if the cache downloaded from url exists 139 | public boolean cacheExistsUrl(String type, String url) { 140 | return cacheExists(type, getCacheName(url)); 141 | } 142 | 143 | public boolean cacheExists(String type, String name) { 144 | return new File(getCachePath(type, name)).isFile(); 145 | } 146 | 147 | public boolean deleteCacheUrl(String type, String url) { 148 | return deleteCache(type, getCacheName(url)); 149 | } 150 | 151 | public boolean deleteCache(String type, String name) { 152 | if (cacheExists(type, name)) { 153 | return new File(getCachePath(type, name)).delete(); 154 | } else { 155 | return false; 156 | } 157 | } 158 | 159 | public InputStream openCacheStream(String type, String name) { 160 | try { 161 | return new FileInputStream(new File(getCachePath(type, name))); 162 | } catch (IOException e) { 163 | return null; 164 | } 165 | } 166 | 167 | public InputStream openCacheStreamUrl(String type, String url) { 168 | return openCacheStream(type, getCacheName(url)); 169 | } 170 | 171 | public Bitmap getBitmap(String type, String name) { 172 | InputStream ipt = openCacheStream(type, name); 173 | 174 | if (ipt == null) return null; 175 | 176 | Bitmap ret = BitmapFactory.decodeStream(ipt); 177 | 178 | try { 179 | ipt.close(); 180 | } catch (IOException e) { 181 | 182 | } 183 | 184 | return ret; 185 | } 186 | 187 | public Bitmap getBitmapUrl(String type, String url) { 188 | return getBitmap(type, getCacheName(url)); 189 | } 190 | 191 | public File getBitmapFile(String type, String name) { 192 | return new File(getCachePath(type, name)); 193 | } 194 | 195 | public File getBitmapUrlFile(String type, String url) { 196 | return getBitmapFile(type, getCacheName(url)); 197 | } 198 | 199 | private String getCacheName(String url) { 200 | return url.replaceAll("/", ".").replaceAll(":", ""); 201 | } 202 | 203 | private String getCachePath(String type, String name) { 204 | return mCacheDir.getAbsolutePath() + "/" + type + "/" + name + ".cache"; 205 | } 206 | 207 | } 208 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/cache/file/OfflineDocumentManager.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.cache.file; 2 | 3 | import android.content.Context; 4 | 5 | import java.io.BufferedReader; 6 | import java.io.IOException; 7 | import java.io.InputStreamReader; 8 | 9 | import moe.feng.nhentai.cache.common.Constants; 10 | 11 | public class OfflineDocumentManager { 12 | 13 | private FileCacheManager mCacheManager; 14 | 15 | private static OfflineDocumentManager sInstance; 16 | 17 | public static OfflineDocumentManager getInstance(Context context) { 18 | if (sInstance == null) { 19 | sInstance = new OfflineDocumentManager(context); 20 | } 21 | 22 | return sInstance; 23 | } 24 | 25 | private OfflineDocumentManager(Context context) { 26 | mCacheManager = FileCacheManager.getInstance(context); 27 | } 28 | 29 | public String getOfflineDocument(String url) { 30 | if (mCacheManager.cacheExistsUrl(Constants.CACHE_DOCUMENT, url)) { 31 | BufferedReader bf = new BufferedReader( 32 | new InputStreamReader(mCacheManager.openCacheStreamUrl(Constants.CACHE_DOCUMENT, url)) 33 | ); 34 | StringBuffer buffer = new StringBuffer(); 35 | String line = ""; 36 | try { 37 | while ((line = bf.readLine()) != null){ 38 | buffer.append(line); 39 | } 40 | } catch (IOException e) { 41 | e.printStackTrace(); 42 | } 43 | return buffer.toString(); 44 | } else { 45 | return null; 46 | } 47 | } 48 | 49 | public boolean hasOfflineCache(String url) { 50 | return mCacheManager.cacheExistsUrl(Constants.CACHE_DOCUMENT, url); 51 | } 52 | 53 | public boolean createOfflineCache(String url) { 54 | return mCacheManager.createCacheFromNetwork(Constants.CACHE_DOCUMENT, url); 55 | } 56 | 57 | } 58 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/dao/CommonPreferences.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.dao; 2 | 3 | import android.content.Context; 4 | 5 | import java.util.ArrayList; 6 | 7 | import sumimakito.android.quickkv.QuickKV; 8 | import sumimakito.android.quickkv.database.KeyValueDatabase; 9 | 10 | public class CommonPreferences { 11 | 12 | private QuickKV mQuickKV; 13 | private KeyValueDatabase mKVDB; 14 | private String mDBName; 15 | 16 | private static ArrayList sInstances = new ArrayList<>(); 17 | 18 | public static CommonPreferences getInstance(Context context, String dbName) { 19 | CommonPreferences sInstance = null; 20 | for (Instance i : sInstances) { 21 | if (i.dbName == dbName) { 22 | sInstance = i.preferences; 23 | break; 24 | } 25 | } 26 | if (sInstance == null) { 27 | sInstance = new CommonPreferences(context, dbName); 28 | sInstances.add(new Instance(sInstance, dbName)); 29 | } 30 | return sInstance; 31 | } 32 | 33 | private CommonPreferences(Context context, String dbName) { 34 | this.mQuickKV = new QuickKV(context); 35 | this.mDBName = dbName; 36 | reload(); 37 | } 38 | 39 | public void sync() { 40 | this.mKVDB.sync(true); 41 | } 42 | 43 | public void reload() { 44 | this.mKVDB = this.mQuickKV.getDatabase("prefs_" + mDBName); 45 | } 46 | 47 | public Editor edit() { 48 | return new Editor(mKVDB); 49 | } 50 | 51 | public int getInt(String key, int defValue) { 52 | return contains(key) ? (int) mKVDB.get(key) : defValue; 53 | } 54 | 55 | public String getString(String key, String defValue) { 56 | return contains(key) ? (String) mKVDB.get(key) : defValue; 57 | } 58 | 59 | public boolean getBoolean(String key, boolean defValue) { 60 | return contains(key) ? (boolean) mKVDB.get(key) : defValue; 61 | } 62 | 63 | public long getLong(String key, long defValue) { 64 | return contains(key) ? (long) mKVDB.get(key) : defValue; 65 | } 66 | 67 | public float getFloat(String key, float defValue) { 68 | return contains(key) ? (float) mKVDB.get(key) : defValue; 69 | } 70 | 71 | public boolean contains(String key) { 72 | return mKVDB.containsKey(key); 73 | } 74 | 75 | public class Editor { 76 | 77 | private KeyValueDatabase mKVDB; 78 | 79 | private Editor(KeyValueDatabase kvdb) { 80 | this.mKVDB = kvdb; 81 | } 82 | 83 | public Editor putBoolean(String key, boolean value) { 84 | this.mKVDB.put(key, value); 85 | return this; 86 | } 87 | 88 | public Editor putInt(String key, int value) { 89 | this.mKVDB.put(key, value); 90 | return this; 91 | } 92 | 93 | public Editor putString(String key, String value) { 94 | this.mKVDB.put(key, value); 95 | return this; 96 | } 97 | 98 | public Editor putLong(String key, long value) { 99 | this.mKVDB.put(key, value); 100 | return this; 101 | } 102 | 103 | public Editor putFloat(String key, float value) { 104 | this.mKVDB.put(key, value); 105 | return this; 106 | } 107 | 108 | public Editor remove(String key){ 109 | this.mKVDB.remove(key); 110 | return this; 111 | } 112 | 113 | public void clear() { 114 | this.mKVDB.clear(); 115 | this.mKVDB.persist(); 116 | } 117 | 118 | public boolean commit() { 119 | return this.mKVDB.persist(); 120 | } 121 | 122 | } 123 | 124 | private static class Instance { 125 | 126 | CommonPreferences preferences; 127 | String dbName; 128 | 129 | public Instance(CommonPreferences preferences, String dbName) { 130 | this.preferences = preferences; 131 | this.dbName = dbName; 132 | } 133 | 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/dao/SearchHistoryManager.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.dao; 2 | 3 | import android.content.Context; 4 | 5 | import com.quinny898.library.persistentsearch.SearchResult; 6 | 7 | import java.util.ArrayList; 8 | 9 | import moe.feng.nhentai.R; 10 | import sumimakito.android.quickkv.QuickKV; 11 | import sumimakito.android.quickkv.database.KeyValueDatabase; 12 | 13 | public class SearchHistoryManager { 14 | 15 | private QuickKV mQuickKV; 16 | private KeyValueDatabase mDB; 17 | private String mSectionName; 18 | 19 | private static ArrayList sInstances = new ArrayList<>(); 20 | 21 | private static final String DATABASE_NAME = "search_history"; 22 | 23 | public static SearchHistoryManager getInstance(Context context, String sectionName) { 24 | SearchHistoryManager sInstance = null; 25 | for (Instance i : sInstances) { 26 | if (i.sectionName == sectionName) { 27 | sInstance = i.manager; 28 | break; 29 | } 30 | } 31 | if (sInstance == null) { 32 | sInstance = new SearchHistoryManager(context, sectionName); 33 | sInstances.add(new Instance(sInstance, sectionName)); 34 | } 35 | return sInstance; 36 | } 37 | 38 | public SearchHistoryManager(Context context, String sectionName) { 39 | this.mQuickKV = new QuickKV(context); 40 | this.mSectionName = sectionName; 41 | reloadDatabase(); 42 | } 43 | 44 | public void reloadDatabase() { 45 | mDB = mQuickKV.getDatabase(DATABASE_NAME + "_" + mSectionName); 46 | } 47 | 48 | public void add(String keyword) { 49 | int pos = find(keyword); 50 | if (pos < 0) { 51 | pos = 9; 52 | } 53 | moveArrayToNext(pos - 1); 54 | mDB.put("history_0", keyword); 55 | mDB.persist(); 56 | } 57 | 58 | public String get(int pos) { 59 | return (String) mDB.get("history_" + pos); 60 | } 61 | 62 | public int find(String keyword) { 63 | for (int i = 9; i >= 0; i--) { 64 | if (mDB.containsKey("history_" + i)) { 65 | if (mDB.get("history_" + i).equals(keyword)){ 66 | return i; 67 | } 68 | } 69 | } 70 | return -1; 71 | } 72 | 73 | private void moveArrayToNext(int end) { 74 | for (int i = end; i >= 0; i--) { 75 | if (mDB.containsKey("history_" + i)) { 76 | mDB.put("history_" + (i + 1), mDB.get("history_" + i)); 77 | } 78 | } 79 | } 80 | 81 | public void cleanAll() { 82 | mDB.clear(); 83 | mDB.persist(); 84 | } 85 | 86 | public String[] getAll() { 87 | String[] histories = new String[10]; 88 | for (int i = 0; i < 10; i++) { 89 | histories[i] = (String) mDB.get("history_" + i); 90 | } 91 | return histories; 92 | } 93 | 94 | public ArrayList getSearchResults() { 95 | ArrayList results = new ArrayList<>(); 96 | for (String history : getAll()) { 97 | if (history == null) continue; 98 | results.add(new SearchResult(history, R.drawable.ic_history)); 99 | } 100 | return results; 101 | } 102 | 103 | private static class Instance { 104 | 105 | SearchHistoryManager manager; 106 | String sectionName; 107 | 108 | public Instance(SearchHistoryManager manager, String sectionName) { 109 | this.manager = manager; 110 | this.sectionName = sectionName; 111 | } 112 | 113 | } 114 | 115 | } 116 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/model/BaseMessage.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.model; 2 | 3 | public class BaseMessage { 4 | 5 | private int code = -1; 6 | private Object data; 7 | 8 | public BaseMessage(int code, Object data) { 9 | this.code = code; 10 | this.data = data; 11 | } 12 | 13 | public BaseMessage() { 14 | 15 | } 16 | 17 | public int getCode() { 18 | return code; 19 | } 20 | 21 | public void setCode(int code) { 22 | this.code = code; 23 | } 24 | 25 | public T getData() { 26 | return (T) data; 27 | } 28 | 29 | public void setData(Object data) { 30 | this.data = data; 31 | } 32 | 33 | } 34 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/model/Book.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.model; 2 | 3 | import com.google.gson.Gson; 4 | 5 | import java.util.ArrayList; 6 | 7 | public class Book { 8 | 9 | /** 必须获取到的数据 */ 10 | public String title, other, bookId; 11 | 12 | /** 次要数据 */ 13 | public String previewImageUrl, bigCoverImageUrl, titleJP, galleryId; 14 | public int pageCount; 15 | 16 | public int thumbHeight = 0, thumbWidth = 0; 17 | 18 | public String parodies, language, artist, group; 19 | public ArrayList tags = new ArrayList<>(); 20 | public ArrayList characters = new ArrayList<>(); 21 | 22 | public String uploadTime, uploadTimeText; 23 | 24 | public Book() { 25 | this(null, null, null); 26 | } 27 | 28 | public Book(String title, String other, String bookId) { 29 | this.title = title; 30 | this.other = other; 31 | this.bookId = bookId; 32 | } 33 | 34 | public Book(String title, String other, String bookId, String previewImageUrl) { 35 | this.title = title; 36 | this.other = other; 37 | this.bookId = bookId; 38 | this.previewImageUrl = previewImageUrl; 39 | } 40 | 41 | public String toJSONString() { 42 | return new Gson().toJson(this); 43 | } 44 | 45 | } 46 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/CategoryActivity.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui; 2 | 3 | import android.content.Intent; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v4.widget.SwipeRefreshLayout; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.support.v7.widget.StaggeredGridLayoutManager; 11 | 12 | import java.util.ArrayList; 13 | 14 | import moe.feng.nhentai.R; 15 | import moe.feng.nhentai.api.PageApi; 16 | import moe.feng.nhentai.model.BaseMessage; 17 | import moe.feng.nhentai.model.Book; 18 | import moe.feng.nhentai.ui.adapter.BookListRecyclerAdapter; 19 | import moe.feng.nhentai.ui.common.AbsActivity; 20 | import moe.feng.nhentai.ui.common.AbsRecyclerViewAdapter; 21 | import moe.feng.nhentai.util.AsyncTask; 22 | 23 | public class CategoryActivity extends AbsActivity { 24 | 25 | private RecyclerView mRecyclerView; 26 | private BookListRecyclerAdapter mAdapter; 27 | private StaggeredGridLayoutManager mLayoutManager; 28 | 29 | private SwipeRefreshLayout mSwipeRefreshLayout; 30 | 31 | private ArrayList mBooks; 32 | 33 | private int mNowPage = 1; 34 | private String url, title; 35 | 36 | private static final String EXTRA_URL = "url", EXTRA_TITLE = "title"; 37 | 38 | public static final String TAG = CategoryActivity.class.getSimpleName(); 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | 44 | Intent intent = getIntent(); 45 | url = intent.getStringExtra(EXTRA_URL); 46 | title = intent.getStringExtra(EXTRA_TITLE); 47 | 48 | setContentView(R.layout.activity_search_result); 49 | 50 | mActionBar.setDisplayHomeAsUpEnabled(true); 51 | mActionBar.setTitle(title); 52 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 53 | mActionBar.setElevation(getResources().getDimension(R.dimen.appbar_elevation)); 54 | } 55 | 56 | mSwipeRefreshLayout.setRefreshing(true); 57 | new PageGetTask().execute(mNowPage); 58 | } 59 | 60 | @Override 61 | protected void setUpViews() { 62 | mRecyclerView = $(R.id.recycler_view); 63 | mSwipeRefreshLayout = $(R.id.swipe_refresh_layout); 64 | 65 | mLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 66 | mRecyclerView.setLayoutManager(mLayoutManager); 67 | mRecyclerView.setHasFixedSize(true); 68 | 69 | mBooks = new ArrayList<>(); 70 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, mBooks); 71 | setRecyclerViewAdapter(mAdapter); 72 | 73 | mSwipeRefreshLayout.setColorSchemeResources( 74 | R.color.deep_purple_500, R.color.pink_500, R.color.orange_500, R.color.brown_500, 75 | R.color.indigo_500, R.color.blue_500, R.color.teal_500, R.color.green_500 76 | ); 77 | mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 78 | @Override 79 | public void onRefresh() { 80 | if (!mSwipeRefreshLayout.isRefreshing()) { 81 | mSwipeRefreshLayout.setRefreshing(true); 82 | } 83 | 84 | mBooks = new ArrayList<>(); 85 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, mBooks); 86 | setRecyclerViewAdapter(mAdapter); 87 | new PageGetTask().execute(mNowPage = 1); 88 | } 89 | }); 90 | } 91 | 92 | 93 | private void setRecyclerViewAdapter(BookListRecyclerAdapter adapter) { 94 | adapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 95 | @Override 96 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 97 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 98 | BookDetailsActivity.launch(CategoryActivity.this, holder.mPreviewImageView, holder.book); 99 | } 100 | }); 101 | adapter.addOnScrollListener(new RecyclerView.OnScrollListener() { 102 | @Override 103 | public void onScrolled(RecyclerView rv, int dx, int dy) { 104 | if (!mSwipeRefreshLayout.isRefreshing() && mLayoutManager.findLastCompletelyVisibleItemPositions(new int[2])[1] >= mAdapter.getItemCount() - 2) { 105 | mSwipeRefreshLayout.setRefreshing(true); 106 | new PageGetTask().execute(++mNowPage); 107 | } 108 | } 109 | }); 110 | 111 | mRecyclerView.setAdapter(adapter); 112 | } 113 | 114 | private class PageGetTask extends AsyncTask { 115 | 116 | @Override 117 | protected BaseMessage doInBackground(Integer... params) { 118 | return PageApi.getPageList(url + "/?page=" + mNowPage); 119 | } 120 | 121 | @Override 122 | protected void onPostExecute(BaseMessage msg) { 123 | mSwipeRefreshLayout.setRefreshing(false); 124 | if (msg != null) { 125 | if (msg.getCode() == 0 && msg.getData() != null) { 126 | if (!((ArrayList) msg.getData()).isEmpty()) { 127 | mBooks.addAll((ArrayList) msg.getData()); 128 | mAdapter.notifyDataSetChanged(); 129 | if (mNowPage == 1) { 130 | mRecyclerView.setAdapter(mAdapter); 131 | } 132 | } else { 133 | Snackbar.make(mRecyclerView, R.string.tips_no_result, Snackbar.LENGTH_LONG).show(); 134 | } 135 | } else if (mNowPage == 1) { 136 | Snackbar.make(mRecyclerView, R.string.tips_no_result, Snackbar.LENGTH_LONG).show(); 137 | } 138 | } 139 | } 140 | 141 | } 142 | 143 | public static void launch(AppCompatActivity activity, String url, String title) { 144 | Intent intent = new Intent(activity, CategoryActivity.class); 145 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 146 | intent.putExtra(EXTRA_URL, url); 147 | intent.putExtra(EXTRA_TITLE, title); 148 | activity.startActivity(intent); 149 | } 150 | 151 | } 152 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/GalleryActivity.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui; 2 | 3 | import android.app.Activity; 4 | import android.content.Intent; 5 | import android.graphics.Color; 6 | import android.os.Build; 7 | import android.os.Bundle; 8 | import android.support.v4.view.ViewPager; 9 | import android.view.View; 10 | import android.view.WindowManager; 11 | 12 | import com.google.gson.Gson; 13 | 14 | import moe.feng.nhentai.R; 15 | import moe.feng.nhentai.model.Book; 16 | import moe.feng.nhentai.ui.adapter.GalleryPagerAdapter; 17 | import moe.feng.nhentai.ui.common.AbsActivity; 18 | import moe.feng.nhentai.util.FullScreenHelper; 19 | 20 | public class GalleryActivity extends AbsActivity { 21 | 22 | private Book book; 23 | private int page_num; 24 | 25 | private ViewPager mPager; 26 | private GalleryPagerAdapter mPagerAdpater; 27 | private View mAppBar; 28 | 29 | private FullScreenHelper mFullScreenHelper; 30 | 31 | private static final String EXTRA_BOOK_DATA = "book_data", EXTRA_FISRT_PAGE = "first_page"; 32 | 33 | @Override 34 | protected void onCreate(Bundle savedInstanceState) { 35 | super.onCreate(savedInstanceState); 36 | 37 | if (Build.VERSION.SDK_INT >= 21) { 38 | getWindow().setStatusBarColor(Color.TRANSPARENT); 39 | getWindow().setNavigationBarColor(Color.TRANSPARENT); 40 | } 41 | 42 | mFullScreenHelper = new FullScreenHelper(this); 43 | // 别问我为什么这么干 让我先冷静一下→_→ 44 | mFullScreenHelper.setFullScreen(true); 45 | mFullScreenHelper.setFullScreen(false); 46 | 47 | Intent intent = getIntent(); 48 | book = new Gson().fromJson(intent.getStringExtra(EXTRA_BOOK_DATA), Book.class); 49 | page_num = intent.getIntExtra(EXTRA_FISRT_PAGE, 0); 50 | 51 | setContentView(R.layout.activity_gallery); 52 | } 53 | 54 | @Override 55 | protected void setUpViews() { 56 | getSupportActionBar().setDisplayHomeAsUpEnabled(true); 57 | getSupportActionBar().setTitle(book.titleJP != null ? book.titleJP : book.title); 58 | 59 | mAppBar = $(R.id.my_app_bar); 60 | mPager = $(R.id.pager); 61 | mPagerAdpater = new GalleryPagerAdapter(getFragmentManager(), book); 62 | mPager.setAdapter(mPagerAdpater); 63 | mPager.setCurrentItem(page_num, false); 64 | } 65 | 66 | public static void launch(Activity activity, Book book, int firstPageNum) { 67 | Intent intent = new Intent(activity, GalleryActivity.class); 68 | intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 69 | intent.putExtra(EXTRA_BOOK_DATA, book.toJSONString()); 70 | intent.putExtra(EXTRA_FISRT_PAGE, firstPageNum); 71 | activity.startActivity(intent); 72 | } 73 | 74 | public void toggleControlBar() { 75 | if (mAppBar.getAlpha() != 0f) { 76 | mAppBar.animate().alpha(0f).start(); 77 | mFullScreenHelper.setFullScreen(true); 78 | } else if (mAppBar.getAlpha() != 1f) { 79 | mAppBar.animate().alpha(1f).start(); 80 | mFullScreenHelper.setFullScreen(false); 81 | } 82 | } 83 | 84 | @Override 85 | public void onBackPressed() { 86 | if (mAppBar.getAlpha() != 1f) { 87 | toggleControlBar(); 88 | } else { 89 | super.onBackPressed(); 90 | } 91 | } 92 | 93 | } 94 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/SearchResultActivity.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui; 2 | 3 | import android.content.Intent; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.support.design.widget.Snackbar; 7 | import android.support.v4.widget.SwipeRefreshLayout; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.RecyclerView; 10 | import android.support.v7.widget.StaggeredGridLayoutManager; 11 | 12 | import java.util.ArrayList; 13 | 14 | import moe.feng.nhentai.R; 15 | import moe.feng.nhentai.api.PageApi; 16 | import moe.feng.nhentai.model.BaseMessage; 17 | import moe.feng.nhentai.model.Book; 18 | import moe.feng.nhentai.ui.adapter.BookListRecyclerAdapter; 19 | import moe.feng.nhentai.ui.common.AbsActivity; 20 | import moe.feng.nhentai.ui.common.AbsRecyclerViewAdapter; 21 | import moe.feng.nhentai.util.AsyncTask; 22 | 23 | public class SearchResultActivity extends AbsActivity { 24 | 25 | private RecyclerView mRecyclerView; 26 | private BookListRecyclerAdapter mAdapter; 27 | private StaggeredGridLayoutManager mLayoutManager; 28 | 29 | private SwipeRefreshLayout mSwipeRefreshLayout; 30 | 31 | private ArrayList mBooks; 32 | 33 | private int mNowPage = 1; 34 | private String keyword; 35 | 36 | private static final String EXTRA_KEYWORD = "keyword"; 37 | 38 | public static final String TAG = SearchResultActivity.class.getSimpleName(); 39 | 40 | @Override 41 | protected void onCreate(Bundle savedInstanceState) { 42 | super.onCreate(savedInstanceState); 43 | 44 | Intent intent = getIntent(); 45 | keyword = intent.getStringExtra(EXTRA_KEYWORD); 46 | 47 | setContentView(R.layout.activity_search_result); 48 | 49 | mActionBar.setDisplayHomeAsUpEnabled(true); 50 | mActionBar.setTitle(String.format(getString(R.string.title_search_result), keyword)); 51 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 52 | mActionBar.setElevation(getResources().getDimension(R.dimen.appbar_elevation)); 53 | } 54 | 55 | mSwipeRefreshLayout.setRefreshing(true); 56 | new PageGetTask().execute(mNowPage); 57 | } 58 | 59 | @Override 60 | protected void setUpViews() { 61 | mRecyclerView = $(R.id.recycler_view); 62 | mSwipeRefreshLayout = $(R.id.swipe_refresh_layout); 63 | 64 | mLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 65 | mRecyclerView.setLayoutManager(mLayoutManager); 66 | mRecyclerView.setHasFixedSize(true); 67 | 68 | mBooks = new ArrayList<>(); 69 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, mBooks); 70 | setRecyclerViewAdapter(mAdapter); 71 | 72 | mSwipeRefreshLayout.setColorSchemeResources( 73 | R.color.deep_purple_500, R.color.pink_500, R.color.orange_500, R.color.brown_500, 74 | R.color.indigo_500, R.color.blue_500, R.color.teal_500, R.color.green_500 75 | ); 76 | mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 77 | @Override 78 | public void onRefresh() { 79 | if (!mSwipeRefreshLayout.isRefreshing()) { 80 | mSwipeRefreshLayout.setRefreshing(true); 81 | } 82 | 83 | mBooks = new ArrayList<>(); 84 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, mBooks); 85 | setRecyclerViewAdapter(mAdapter); 86 | new PageGetTask().execute(mNowPage = 1); 87 | } 88 | }); 89 | } 90 | 91 | 92 | private void setRecyclerViewAdapter(BookListRecyclerAdapter adapter) { 93 | adapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 94 | @Override 95 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 96 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 97 | BookDetailsActivity.launch(SearchResultActivity.this, holder.mPreviewImageView, holder.book); 98 | } 99 | }); 100 | adapter.addOnScrollListener(new RecyclerView.OnScrollListener() { 101 | @Override 102 | public void onScrolled(RecyclerView rv, int dx, int dy) { 103 | if (!mSwipeRefreshLayout.isRefreshing() && mLayoutManager.findLastCompletelyVisibleItemPositions(new int[2])[1] >= mAdapter.getItemCount() - 2) { 104 | mSwipeRefreshLayout.setRefreshing(true); 105 | new PageGetTask().execute(++mNowPage); 106 | } 107 | } 108 | }); 109 | 110 | mRecyclerView.setAdapter(adapter); 111 | } 112 | 113 | private class PageGetTask extends AsyncTask { 114 | 115 | @Override 116 | protected BaseMessage doInBackground(Integer... params) { 117 | return PageApi.getSearchPageList(keyword, params[0]); 118 | } 119 | 120 | @Override 121 | protected void onPostExecute(BaseMessage msg) { 122 | mSwipeRefreshLayout.setRefreshing(false); 123 | if (msg != null) { 124 | if (msg.getCode() == 0 && msg.getData() != null) { 125 | if (!((ArrayList) msg.getData()).isEmpty()) { 126 | mBooks.addAll((ArrayList) msg.getData()); 127 | mAdapter.notifyDataSetChanged(); 128 | if (mNowPage == 1) { 129 | mRecyclerView.setAdapter(mAdapter); 130 | } 131 | } else { 132 | Snackbar.make(mRecyclerView, R.string.tips_no_result, Snackbar.LENGTH_LONG).show(); 133 | } 134 | } else if (mNowPage == 1) { 135 | Snackbar.make(mRecyclerView, R.string.tips_no_result, Snackbar.LENGTH_LONG).show(); 136 | } 137 | } 138 | } 139 | 140 | } 141 | 142 | public static void launch(AppCompatActivity activity, String keyword) { 143 | Intent intent = new Intent(activity, SearchResultActivity.class); 144 | intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); 145 | intent.putExtra(EXTRA_KEYWORD, keyword); 146 | activity.startActivity(intent); 147 | } 148 | 149 | } 150 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/SettingsActivity.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui; 2 | 3 | import android.app.Activity; 4 | import android.app.Fragment; 5 | import android.content.Intent; 6 | import android.os.Bundle; 7 | import android.support.v4.view.ViewCompat; 8 | 9 | import moe.feng.nhentai.R; 10 | import moe.feng.nhentai.ui.common.AbsActivity; 11 | import moe.feng.nhentai.ui.fragment.settings.SettingsLicense; 12 | import moe.feng.nhentai.ui.fragment.settings.SettingsMain; 13 | 14 | public class SettingsActivity extends AbsActivity { 15 | 16 | private Fragment mFragment; 17 | private int flag; 18 | 19 | public static final String EXTRA_FLAG = "flag"; 20 | public static final int FLAG_MAIN = 0, FLAG_LICENSE = 1, FLAG_GUI = 2, FLAG_NETWORK = 3; 21 | 22 | @Override 23 | public void onCreate(Bundle savedInstanceState) { 24 | super.onCreate(savedInstanceState); 25 | 26 | Intent intent = getIntent(); 27 | flag = intent.getIntExtra(EXTRA_FLAG, FLAG_MAIN); 28 | 29 | setContentView(R.layout.activity_settings); 30 | 31 | mActionBar.setDisplayHomeAsUpEnabled(true); 32 | } 33 | 34 | @Override 35 | public void setUpViews() { 36 | ViewCompat.setElevation(mToolbar, getResources().getDimension(R.dimen.appbar_elevation)); 37 | 38 | switch (flag) { 39 | case FLAG_MAIN: 40 | mFragment = new SettingsMain(); 41 | break; 42 | case FLAG_LICENSE: 43 | mFragment = new SettingsLicense(); 44 | break; 45 | } 46 | getFragmentManager().beginTransaction() 47 | .replace(R.id.container, mFragment) 48 | .commit(); 49 | } 50 | 51 | 52 | public static void launchActivity(Activity mActivity, int flag) { 53 | Intent intent = new Intent(mActivity, SettingsActivity.class); 54 | intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK); 55 | intent.putExtra(EXTRA_FLAG, flag); 56 | mActivity.startActivity(intent); 57 | } 58 | 59 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/adapter/BookListRecyclerAdapter.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.adapter; 2 | 3 | import android.graphics.Bitmap; 4 | import android.support.v7.widget.ListPopupWindow; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.text.TextUtils; 7 | import android.util.Log; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.view.ViewTreeObserver; 12 | import android.widget.AdapterView; 13 | import android.widget.ImageView; 14 | import android.widget.TextView; 15 | 16 | import java.util.ArrayList; 17 | 18 | import moe.feng.nhentai.R; 19 | import moe.feng.nhentai.api.BookApi; 20 | import moe.feng.nhentai.model.Book; 21 | import moe.feng.nhentai.ui.common.AbsRecyclerViewAdapter; 22 | import moe.feng.nhentai.util.AsyncTask; 23 | import moe.feng.nhentai.util.ColorGenerator; 24 | import moe.feng.nhentai.util.TextDrawable; 25 | import moe.feng.nhentai.util.Utility; 26 | 27 | public class BookListRecyclerAdapter extends AbsRecyclerViewAdapter { 28 | 29 | private ArrayList data; 30 | 31 | private ColorGenerator mColorGenerator; 32 | 33 | public static final String TAG = BookListRecyclerAdapter.class.getSimpleName(); 34 | 35 | public BookListRecyclerAdapter(RecyclerView recyclerView, ArrayList data) { 36 | super(recyclerView); 37 | this.data = data; 38 | mColorGenerator = ColorGenerator.MATERIAL; 39 | } 40 | 41 | @Override 42 | public ClickableViewHolder onCreateViewHolder(ViewGroup viewGroup, int viewType) { 43 | bindContext(viewGroup.getContext()); 44 | View view = LayoutInflater.from(getContext()).inflate(R.layout.list_item_book_card, viewGroup, false); 45 | return new ViewHolder(view); 46 | } 47 | 48 | @Override 49 | public void onBindViewHolder(ClickableViewHolder holder, final int position) { 50 | super.onBindViewHolder(holder, position); 51 | if (holder instanceof ViewHolder) { 52 | final ViewHolder mHolder = (ViewHolder) holder; 53 | mHolder.mTitleTextView.setText(data.get(position).title); 54 | String previewImageUrl = data.get(position).previewImageUrl; 55 | 56 | int color = mColorGenerator.getColor(data.get(position).title); 57 | TextDrawable drawable = TextDrawable.builder().buildRect(Utility.getFirstCharacter(data.get(position).title), color); 58 | mHolder.mPreviewImageView.setImageDrawable(drawable); 59 | 60 | if (previewImageUrl != null) { 61 | switch (previewImageUrl) { 62 | case "0": 63 | mHolder.mPreviewImageView.setImageResource(R.drawable.holder_0); 64 | break; 65 | case "1": 66 | mHolder.mPreviewImageView.setImageResource(R.drawable.holder_1); 67 | break; 68 | case "2": 69 | mHolder.mPreviewImageView.setImageResource(R.drawable.holder_2); 70 | break; 71 | default: 72 | ViewTreeObserver vto = mHolder.mPreviewImageView.getViewTreeObserver(); 73 | vto.addOnGlobalLayoutListener(new ViewTreeObserver.OnGlobalLayoutListener() { 74 | @Override 75 | public void onGlobalLayout() { 76 | int thumbWidth = data.get(position).thumbWidth; 77 | int thumbHeight = data.get(position).thumbHeight; 78 | if (thumbWidth > 0 && thumbHeight > 0) { 79 | int width = mHolder.mPreviewImageView.getMeasuredWidth(); 80 | int height = Math.round(width * ((float) thumbHeight / thumbWidth)); 81 | mHolder.mPreviewImageView.getLayoutParams().height = height; 82 | mHolder.mPreviewImageView.setMinimumHeight(height); 83 | } 84 | mHolder.mPreviewImageView.getViewTreeObserver().removeGlobalOnLayoutListener(this); 85 | 86 | } 87 | }); 88 | new ImageDownloader().execute(mHolder.getParentView()); 89 | } 90 | } 91 | 92 | mHolder.book = data.get(position); 93 | } 94 | } 95 | 96 | @Override 97 | public int getItemCount() { 98 | return data.size(); 99 | } 100 | 101 | private class ImageDownloader extends AsyncTask { 102 | 103 | @Override 104 | protected Void doInBackground(Object[] params) { 105 | View v = (View) params[0]; 106 | ViewHolder h = (ViewHolder) v.getTag(); 107 | Book book = h.book; 108 | 109 | if (v != null && !TextUtils.isEmpty(book.previewImageUrl)) { 110 | ImageView imgView = h.mPreviewImageView; 111 | 112 | Bitmap img = BookApi.getThumb(getContext(), book); 113 | 114 | if (img != null) { 115 | publishProgress(new Object[]{v, img, imgView, book}); 116 | } 117 | } 118 | 119 | return null; 120 | } 121 | 122 | @Override 123 | protected void onProgressUpdate(Object[] values) { 124 | super.onProgressUpdate(values); 125 | 126 | View v = (View) values[0]; 127 | 128 | if (!(v.getTag() instanceof ViewHolder) || (((ViewHolder) v.getTag()).book != null && 129 | ((ViewHolder) v.getTag()).book.bookId != ((Book) values[3]).bookId)) { 130 | return; 131 | } 132 | 133 | Bitmap img = (Bitmap) values[1]; 134 | ImageView iv = (ImageView) values[2]; 135 | iv.setVisibility(View.VISIBLE); 136 | iv.setImageBitmap(img); 137 | iv.setTag(false); 138 | } 139 | 140 | 141 | } 142 | 143 | public class ViewHolder extends ClickableViewHolder { 144 | 145 | public ImageView mPreviewImageView; 146 | public TextView mTitleTextView; 147 | 148 | public Book book; 149 | 150 | public ViewHolder(View itemView) { 151 | super(itemView); 152 | mPreviewImageView = (ImageView) itemView.findViewById(R.id.book_preview); 153 | mTitleTextView = (TextView) itemView.findViewById(R.id.book_title); 154 | 155 | itemView.setTag(this); 156 | } 157 | 158 | } 159 | 160 | } 161 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/adapter/BookPreviewGridAdapter.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.adapter; 2 | 3 | import android.content.Context; 4 | import android.graphics.Bitmap; 5 | import android.text.TextUtils; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.widget.BaseAdapter; 9 | import android.widget.GridView; 10 | import android.widget.ImageView; 11 | import android.widget.TextView; 12 | 13 | import moe.feng.nhentai.R; 14 | import moe.feng.nhentai.api.BookApi; 15 | import moe.feng.nhentai.model.Book; 16 | import moe.feng.nhentai.util.AsyncTask; 17 | 18 | public class BookPreviewGridAdapter extends BaseAdapter { 19 | 20 | private Book book; 21 | private Context mContext; 22 | 23 | public BookPreviewGridAdapter(Context context, Book book) { 24 | super(); 25 | this.mContext = context; 26 | this.book = book; 27 | } 28 | 29 | @Override 30 | public int getCount() { 31 | return book.pageCount; 32 | } 33 | 34 | @Override 35 | public Object getItem(int i) { 36 | return null; 37 | } 38 | 39 | @Override 40 | public long getItemId(int i) { 41 | return 0; 42 | } 43 | 44 | @Override 45 | public View getView(int position, View view, ViewGroup viewGroup) { 46 | ViewHolder holder; 47 | if (view == null) { 48 | view = View.inflate(mContext, R.layout.list_item_book_picture_thumb, null); 49 | 50 | holder = new ViewHolder(view); 51 | view.setTag(holder); 52 | } else { 53 | holder = (ViewHolder) view.getTag(); 54 | } 55 | 56 | holder.mImageView.setLayoutParams(new GridView.LayoutParams(300, 100)); 57 | holder.mNumberText.setText(position); 58 | 59 | new ImageDownloader().execute(holder.mImageView, position); 60 | 61 | return view; 62 | } 63 | 64 | private class ViewHolder { 65 | 66 | View mParentView; 67 | ImageView mImageView; 68 | TextView mNumberText; 69 | 70 | public ViewHolder(View itemView) { 71 | this.mParentView = itemView; 72 | this.mImageView = (ImageView) itemView.findViewById(R.id.image_view); 73 | this.mNumberText = (TextView) itemView.findViewById(R.id.number_text); 74 | } 75 | 76 | } 77 | 78 | private class ImageDownloader extends AsyncTask { 79 | 80 | @Override 81 | protected Void doInBackground(Object[] params) { 82 | View v = (View) params[0]; 83 | ViewHolder h = (ViewHolder) v.getTag(); 84 | 85 | if (v != null && !TextUtils.isEmpty(book.previewImageUrl)) { 86 | ImageView imgView = h.mImageView; 87 | 88 | Bitmap img = BookApi.getPageThumb(mContext, book, (int) params[1]); 89 | 90 | if (img != null) { 91 | publishProgress(new Object[]{v, img, imgView, book}); 92 | } 93 | } 94 | 95 | return null; 96 | } 97 | 98 | @Override 99 | protected void onProgressUpdate(Object[] values) { 100 | super.onProgressUpdate(values); 101 | 102 | View v = (View) values[0]; 103 | 104 | if (!(v.getTag() instanceof ViewHolder)) { 105 | return; 106 | } 107 | 108 | Bitmap img = (Bitmap) values[1]; 109 | ImageView iv = (ImageView) values[2]; 110 | iv.setVisibility(View.VISIBLE); 111 | iv.setImageBitmap(img); 112 | iv.setTag(false); 113 | } 114 | 115 | 116 | } 117 | 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/adapter/GalleryPagerAdapter.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.adapter; 2 | 3 | import android.app.Fragment; 4 | import android.app.FragmentManager; 5 | import android.support.v13.app.FragmentPagerAdapter; 6 | 7 | import moe.feng.nhentai.model.Book; 8 | import moe.feng.nhentai.ui.fragment.BookPageFragment; 9 | 10 | public class GalleryPagerAdapter extends FragmentPagerAdapter { 11 | 12 | private Book book; 13 | private Fragment[] fragments; 14 | 15 | public GalleryPagerAdapter(FragmentManager fm, Book book) { 16 | super(fm); 17 | this.book = book; 18 | this.fragments = new Fragment[book.pageCount]; 19 | } 20 | 21 | @Override 22 | public Fragment getItem(int position) { 23 | if (fragments[position] == null) { 24 | fragments[position] = BookPageFragment.newInstance(book, position + 1); 25 | } 26 | return fragments[position]; 27 | } 28 | 29 | @Override 30 | public int getCount() { 31 | return book.pageCount; 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/adapter/HomePagerAdapter.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.adapter; 2 | 3 | import android.app.Fragment; 4 | import android.app.FragmentManager; 5 | import android.content.Context; 6 | import android.support.v13.app.FragmentPagerAdapter; 7 | 8 | import moe.feng.nhentai.R; 9 | import moe.feng.nhentai.ui.fragment.main.DownloadManagerFragment; 10 | import moe.feng.nhentai.ui.fragment.main.FavoriteFragment; 11 | import moe.feng.nhentai.ui.fragment.main.HomeFragment; 12 | 13 | public class HomePagerAdapter extends FragmentPagerAdapter { 14 | 15 | private HomeFragment homeFragment; 16 | private DownloadManagerFragment downloadManagerFragment; 17 | private FavoriteFragment favoriteFragment; 18 | 19 | private String[] titles; 20 | 21 | public HomePagerAdapter(Context context, FragmentManager fm) { 22 | super(fm); 23 | titles = context.getResources().getStringArray(R.array.page_titles); 24 | homeFragment = new HomeFragment(); 25 | downloadManagerFragment = new DownloadManagerFragment(); 26 | favoriteFragment = new FavoriteFragment(); 27 | } 28 | 29 | @Override 30 | public Fragment getItem(int position) { 31 | switch (position) { 32 | case 0: 33 | return homeFragment; 34 | case 1: 35 | return downloadManagerFragment; 36 | case 2: 37 | return favoriteFragment; 38 | default: 39 | return null; 40 | } 41 | } 42 | 43 | @Override 44 | public int getCount() { 45 | return 3; 46 | } 47 | 48 | @Override 49 | public CharSequence getPageTitle(int position) { 50 | return titles[position]; 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/common/AbsActivity.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.common; 2 | 3 | import android.graphics.Color; 4 | import android.os.Build; 5 | import android.os.Bundle; 6 | import android.support.annotation.LayoutRes; 7 | import android.support.v7.app.ActionBar; 8 | import android.support.v7.app.AppCompatActivity; 9 | import android.support.v7.widget.Toolbar; 10 | import android.util.Log; 11 | import android.view.MenuItem; 12 | import android.view.View; 13 | 14 | import moe.feng.nhentai.R; 15 | import moe.feng.nhentai.util.Utility; 16 | 17 | public abstract class AbsActivity extends AppCompatActivity { 18 | 19 | protected Toolbar mToolbar; 20 | protected ActionBar mActionBar; 21 | 22 | protected int statusBarHeight = 0; 23 | 24 | @Override 25 | protected void onCreate(Bundle savedInstanceState) { 26 | this.onCreate(savedInstanceState, true); 27 | } 28 | 29 | protected void onCreate(Bundle savedInstanceState, boolean statusBarTranslucent) { 30 | /** Set up translucent status bar */ 31 | if (statusBarTranslucent) { 32 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && !Utility.isChrome()) { 33 | getWindow().getDecorView().setSystemUiVisibility(View.SYSTEM_UI_FLAG_LAYOUT_STABLE | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN); 34 | statusBarHeight = Utility.getStatusBarHeight(getApplicationContext()); 35 | } 36 | 37 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { 38 | getWindow().setStatusBarColor(Color.TRANSPARENT); 39 | getWindow().setNavigationBarColor(getResources().getColor(R.color.deep_purple_800)); 40 | } 41 | } 42 | 43 | super.onCreate(savedInstanceState); 44 | } 45 | 46 | protected abstract void setUpViews(); 47 | 48 | @Override 49 | public void setContentView(@LayoutRes int layoutResId) { 50 | super.setContentView(layoutResId); 51 | 52 | try { 53 | View statusHeaderView = $(R.id.status_bar_header); 54 | statusHeaderView.getLayoutParams().height = statusBarHeight; 55 | } catch (NullPointerException e) { 56 | Log.e("setContentView", "Cannot find status header."); 57 | } 58 | 59 | try { 60 | mToolbar = $(R.id.toolbar); 61 | setSupportActionBar(mToolbar); 62 | } catch (Exception e) { 63 | Log.e("setContentView", "Cannot find toolbar."); 64 | } 65 | mActionBar = getSupportActionBar(); 66 | 67 | setUpViews(); 68 | } 69 | 70 | @Override 71 | public boolean onOptionsItemSelected(MenuItem item) { 72 | if (item.getItemId() == android.R.id.home) { 73 | this.onBackPressed(); 74 | return true; 75 | } 76 | 77 | return super.onOptionsItemSelected(item); 78 | } 79 | 80 | protected T $(int id) { 81 | return (T) findViewById(id); 82 | } 83 | 84 | } 85 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/common/AbsRecyclerViewAdapter.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.common; 2 | 3 | import android.content.Context; 4 | import android.support.v7.widget.RecyclerView; 5 | import android.view.View; 6 | 7 | import java.util.ArrayList; 8 | import java.util.List; 9 | 10 | public abstract class AbsRecyclerViewAdapter extends RecyclerView.Adapter { 11 | 12 | private Context context; 13 | 14 | protected RecyclerView mRecyclerView; 15 | protected List mListeners = new ArrayList(); 16 | 17 | public AbsRecyclerViewAdapter(RecyclerView recyclerView) { 18 | this.mRecyclerView = recyclerView; 19 | this.mRecyclerView.addOnScrollListener(new RecyclerView.OnScrollListener() { 20 | @Override 21 | public void onScrollStateChanged(RecyclerView rv, int newState) { 22 | for (RecyclerView.OnScrollListener listener : mListeners) { 23 | listener.onScrollStateChanged(rv, newState); 24 | } 25 | } 26 | 27 | @Override 28 | public void onScrolled(RecyclerView rv, int dx, int dy) { 29 | for (RecyclerView.OnScrollListener listener : mListeners) { 30 | listener.onScrolled(rv, dx, dy); 31 | } 32 | } 33 | }); 34 | } 35 | 36 | public void addOnScrollListener(RecyclerView.OnScrollListener listener) { 37 | mListeners.add(listener); 38 | } 39 | 40 | public interface OnItemClickListener { 41 | public void onItemClick(int position, ClickableViewHolder holder); 42 | } 43 | 44 | public interface OnItemLongClickListener { 45 | public boolean onItemLongClick(int position, ClickableViewHolder holder); 46 | } 47 | 48 | private OnItemClickListener itemClickListener; 49 | private OnItemLongClickListener itemLongClickListener; 50 | 51 | public void setOnItemClickListener(OnItemClickListener listener) { 52 | this.itemClickListener = listener; 53 | } 54 | 55 | public void setOnItemLongClickListener(OnItemLongClickListener listener) { 56 | this.itemLongClickListener = listener; 57 | } 58 | 59 | public void bindContext(Context context) { 60 | this.context = context; 61 | } 62 | 63 | public Context getContext() { 64 | return this.context; 65 | } 66 | 67 | @Override 68 | public void onBindViewHolder(final ClickableViewHolder holder, final int position) { 69 | holder.getParentView().setOnClickListener(new View.OnClickListener() { 70 | @Override 71 | public void onClick(View v) { 72 | if (itemClickListener != null) { 73 | itemClickListener.onItemClick(position, holder); 74 | } 75 | } 76 | }); 77 | holder.getParentView().setOnLongClickListener(new View.OnLongClickListener() { 78 | @Override 79 | public boolean onLongClick(View v) { 80 | if (itemLongClickListener != null) { 81 | return itemLongClickListener.onItemLongClick(position, holder); 82 | } else { 83 | return false; 84 | } 85 | } 86 | }); 87 | } 88 | 89 | public class ClickableViewHolder extends RecyclerView.ViewHolder { 90 | 91 | private View parentView; 92 | 93 | public ClickableViewHolder(View itemView) { 94 | super(itemView); 95 | this.parentView = itemView; 96 | } 97 | 98 | public View getParentView() { 99 | return parentView; 100 | } 101 | 102 | } 103 | 104 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/fragment/BookPageFragment.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.fragment; 2 | 3 | import android.app.Fragment; 4 | import android.graphics.Bitmap; 5 | import android.graphics.PointF; 6 | import android.os.Bundle; 7 | import android.os.Handler; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | import android.widget.ImageView; 12 | 13 | import com.github.florent37.materialimageloading.MaterialImageLoading; 14 | import com.google.gson.Gson; 15 | import com.squareup.picasso.Callback; 16 | import com.squareup.picasso.Picasso; 17 | 18 | import java.io.File; 19 | 20 | import moe.feng.nhentai.R; 21 | import moe.feng.nhentai.api.PageApi; 22 | import moe.feng.nhentai.model.Book; 23 | import moe.feng.nhentai.ui.GalleryActivity; 24 | import moe.feng.nhentai.util.AsyncTask; 25 | import uk.co.senab.photoview.PhotoViewAttacher; 26 | 27 | public class BookPageFragment extends Fragment { 28 | 29 | private Book book; 30 | private int pageNum; 31 | private ImageView mImageView; 32 | private PhotoViewAttacher mPhotoViewAttacher; 33 | 34 | private static final String ARG_BOOK_DATA = "arg_book_data", ARG_PAGE_NUM = "arg_page_num"; 35 | 36 | public static final String TAG = BookPageFragment.class.getSimpleName(); 37 | 38 | public static BookPageFragment newInstance(Book book, int pageNum) { 39 | BookPageFragment fragment = new BookPageFragment(); 40 | Bundle data = new Bundle(); 41 | data.putString(ARG_BOOK_DATA, book.toJSONString()); 42 | data.putInt(ARG_PAGE_NUM, pageNum); 43 | fragment.setArguments(data); 44 | return fragment; 45 | } 46 | 47 | @Override 48 | public void onCreate(Bundle savedInstanceState) { 49 | super.onCreate(savedInstanceState); 50 | Bundle data = getArguments(); 51 | book = new Gson().fromJson(data.getString(ARG_BOOK_DATA), Book.class); 52 | pageNum = data.getInt(ARG_PAGE_NUM); 53 | } 54 | 55 | @Override 56 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { 57 | View view = inflater.inflate(R.layout.fragment_book_page, container, false); 58 | 59 | mImageView = (ImageView) view.findViewById(R.id.image_view); 60 | mPhotoViewAttacher = new PhotoViewAttacher(mImageView); 61 | new DownloadTask().execute(); 62 | 63 | return view; 64 | } 65 | 66 | private class DownloadTask extends AsyncTask { 67 | 68 | @Override 69 | protected File doInBackground(Void... params) { 70 | return PageApi.getPageOriginImageFile(getActivity().getApplicationContext(), book, pageNum); 71 | } 72 | 73 | @Override 74 | protected void onPostExecute(File result) { 75 | super.onPostExecute(result); 76 | 77 | if (result != null) { 78 | Picasso.with(getActivity().getApplicationContext()) 79 | .load(result) 80 | .into(mImageView, new Callback() { 81 | @Override 82 | public void onSuccess() { 83 | MaterialImageLoading.animate(mImageView).setDuration(700).start(); 84 | mPhotoViewAttacher.update(); 85 | mPhotoViewAttacher.setOnViewTapListener(new PhotoViewAttacher.OnViewTapListener() { 86 | @Override 87 | public void onViewTap(View view, float v, float v1) { 88 | ((GalleryActivity) getActivity()).toggleControlBar(); 89 | } 90 | }); 91 | } 92 | 93 | @Override 94 | public void onError() { 95 | 96 | } 97 | }); 98 | } 99 | } 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/fragment/main/DownloadManagerFragment.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.fragment.main; 2 | 3 | import android.app.Fragment; 4 | import android.os.Bundle; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | import android.util.Log; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import java.util.ArrayList; 13 | 14 | import moe.feng.nhentai.R; 15 | import moe.feng.nhentai.model.Book; 16 | import moe.feng.nhentai.ui.BookDetailsActivity; 17 | import moe.feng.nhentai.ui.adapter.BookListRecyclerAdapter; 18 | import moe.feng.nhentai.ui.common.AbsRecyclerViewAdapter; 19 | 20 | public class DownloadManagerFragment extends Fragment { 21 | 22 | private RecyclerView mRecyclerView; 23 | private BookListRecyclerAdapter mAdapter; 24 | 25 | public static final String TAG = DownloadManagerFragment.class.getSimpleName(); 26 | 27 | @Override 28 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { 29 | View view = inflater.inflate(R.layout.fragment_home, container, false); 30 | 31 | mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 32 | mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)); 33 | mRecyclerView.setHasFixedSize(false); 34 | 35 | ArrayList books = new ArrayList<>(); 36 | 37 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, books); 38 | mAdapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 39 | @Override 40 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 41 | if (viewHolder instanceof BookListRecyclerAdapter.ViewHolder) { 42 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 43 | Log.i(TAG, "You clicked position no." + position + " item, " + 44 | "its name is " + holder.mTitleTextView.getText().toString()); 45 | } 46 | } 47 | }); 48 | setRecyclerViewAdapter(mAdapter); 49 | 50 | return view; 51 | } 52 | 53 | 54 | private void setRecyclerViewAdapter(BookListRecyclerAdapter adapter) { 55 | mRecyclerView.setAdapter(mAdapter); 56 | mAdapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 57 | @Override 58 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 59 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 60 | Log.i(TAG, "You clicked position no." + position + " item, " + 61 | "its name is " + holder.mTitleTextView.getText().toString()); 62 | BookDetailsActivity.launch(getActivity(), holder.mPreviewImageView, holder.book); 63 | } 64 | }); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/fragment/main/FavoriteFragment.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.fragment.main; 2 | 3 | import android.app.Fragment; 4 | import android.os.Bundle; 5 | import android.support.v7.widget.RecyclerView; 6 | import android.support.v7.widget.StaggeredGridLayoutManager; 7 | import android.util.Log; 8 | import android.view.LayoutInflater; 9 | import android.view.View; 10 | import android.view.ViewGroup; 11 | 12 | import java.util.ArrayList; 13 | 14 | import moe.feng.nhentai.R; 15 | import moe.feng.nhentai.model.Book; 16 | import moe.feng.nhentai.ui.BookDetailsActivity; 17 | import moe.feng.nhentai.ui.adapter.BookListRecyclerAdapter; 18 | import moe.feng.nhentai.ui.common.AbsRecyclerViewAdapter; 19 | 20 | public class FavoriteFragment extends Fragment { 21 | 22 | private RecyclerView mRecyclerView; 23 | private BookListRecyclerAdapter mAdapter; 24 | 25 | public static final String TAG = FavoriteFragment.class.getSimpleName(); 26 | 27 | @Override 28 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { 29 | View view = inflater.inflate(R.layout.fragment_home, container, false); 30 | 31 | mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 32 | mRecyclerView.setLayoutManager(new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL)); 33 | mRecyclerView.setHasFixedSize(false); 34 | 35 | ArrayList books = new ArrayList<>(); 36 | 37 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, books); 38 | mAdapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 39 | @Override 40 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 41 | if (viewHolder instanceof BookListRecyclerAdapter.ViewHolder) { 42 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 43 | Log.i(TAG, "You clicked position no." + position + " item, " + 44 | "its name is " + holder.mTitleTextView.getText().toString()); 45 | } 46 | } 47 | }); 48 | setRecyclerViewAdapter(mAdapter); 49 | 50 | return view; 51 | } 52 | 53 | 54 | private void setRecyclerViewAdapter(BookListRecyclerAdapter adapter) { 55 | mRecyclerView.setAdapter(mAdapter); 56 | mAdapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 57 | @Override 58 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 59 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 60 | Log.i(TAG, "You clicked position no." + position + " item, " + 61 | "its name is " + holder.mTitleTextView.getText().toString()); 62 | BookDetailsActivity.launch(getActivity(), holder.mPreviewImageView, holder.book); 63 | } 64 | }); 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/fragment/main/HomeFragment.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.fragment.main; 2 | 3 | import android.app.Fragment; 4 | import android.os.Bundle; 5 | import android.support.design.widget.Snackbar; 6 | import android.support.v4.widget.SwipeRefreshLayout; 7 | import android.support.v7.widget.RecyclerView; 8 | import android.support.v7.widget.StaggeredGridLayoutManager; 9 | import android.util.Log; 10 | import android.view.LayoutInflater; 11 | import android.view.View; 12 | import android.view.ViewGroup; 13 | 14 | import java.util.ArrayList; 15 | 16 | import moe.feng.nhentai.R; 17 | import moe.feng.nhentai.api.PageApi; 18 | import moe.feng.nhentai.model.BaseMessage; 19 | import moe.feng.nhentai.model.Book; 20 | import moe.feng.nhentai.ui.BookDetailsActivity; 21 | import moe.feng.nhentai.ui.adapter.BookListRecyclerAdapter; 22 | import moe.feng.nhentai.ui.common.AbsRecyclerViewAdapter; 23 | import moe.feng.nhentai.util.AsyncTask; 24 | 25 | public class HomeFragment extends Fragment { 26 | 27 | private RecyclerView mRecyclerView; 28 | private BookListRecyclerAdapter mAdapter; 29 | private StaggeredGridLayoutManager mLayoutManager; 30 | 31 | private SwipeRefreshLayout mSwipeRefreshLayout; 32 | 33 | private ArrayList mBooks; 34 | 35 | private int mNowPage = 1; 36 | 37 | public static final String TAG = HomeFragment.class.getSimpleName(); 38 | 39 | @Override 40 | public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle state) { 41 | View view = inflater.inflate(R.layout.fragment_home, container, false); 42 | 43 | mSwipeRefreshLayout = (SwipeRefreshLayout) view.findViewById(R.id.swipe_refresh_layout); 44 | mRecyclerView = (RecyclerView) view.findViewById(R.id.recycler_view); 45 | mLayoutManager = new StaggeredGridLayoutManager(2, StaggeredGridLayoutManager.VERTICAL); 46 | mRecyclerView.setLayoutManager(mLayoutManager); 47 | mRecyclerView.setHasFixedSize(true); 48 | 49 | mBooks = new ArrayList<>(); 50 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, mBooks); 51 | setRecyclerViewAdapter(mAdapter); 52 | 53 | mSwipeRefreshLayout.setColorSchemeResources( 54 | R.color.deep_purple_500, R.color.pink_500, R.color.orange_500, R.color.brown_500, 55 | R.color.indigo_500, R.color.blue_500, R.color.teal_500, R.color.green_500 56 | ); 57 | mSwipeRefreshLayout.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() { 58 | @Override 59 | public void onRefresh() { 60 | if (!mSwipeRefreshLayout.isRefreshing()) { 61 | mSwipeRefreshLayout.setRefreshing(true); 62 | } 63 | 64 | mBooks = new ArrayList<>(); 65 | mAdapter = new BookListRecyclerAdapter(mRecyclerView, mBooks); 66 | setRecyclerViewAdapter(mAdapter); 67 | new PageGetTask().execute(mNowPage = 1); 68 | } 69 | }); 70 | 71 | new PageGetTask().execute(mNowPage); 72 | 73 | return view; 74 | } 75 | 76 | private void setRecyclerViewAdapter(BookListRecyclerAdapter adapter) { 77 | adapter.setOnItemClickListener(new AbsRecyclerViewAdapter.OnItemClickListener() { 78 | @Override 79 | public void onItemClick(int position, AbsRecyclerViewAdapter.ClickableViewHolder viewHolder) { 80 | BookListRecyclerAdapter.ViewHolder holder = (BookListRecyclerAdapter.ViewHolder) viewHolder; 81 | Log.i(TAG, "You clicked position no." + position + " item, " + 82 | "its name is " + holder.mTitleTextView.getText().toString()); 83 | BookDetailsActivity.launch(getActivity(), holder.mPreviewImageView, holder.book); 84 | } 85 | }); 86 | adapter.addOnScrollListener(new RecyclerView.OnScrollListener() { 87 | @Override 88 | public void onScrolled(RecyclerView rv, int dx, int dy) { 89 | if (!mSwipeRefreshLayout.isRefreshing() && mLayoutManager.findLastCompletelyVisibleItemPositions(new int[2])[1] >= mAdapter.getItemCount() - 2) { 90 | mSwipeRefreshLayout.setRefreshing(true); 91 | new PageGetTask().execute(++mNowPage); 92 | } 93 | } 94 | }); 95 | 96 | mRecyclerView.setAdapter(adapter); 97 | } 98 | 99 | private class PageGetTask extends AsyncTask { 100 | 101 | @Override 102 | protected BaseMessage doInBackground(Integer... params) { 103 | return PageApi.getHomePageList(params[0]); 104 | } 105 | 106 | @Override 107 | protected void onPostExecute(BaseMessage msg) { 108 | mSwipeRefreshLayout.setRefreshing(false); 109 | if (msg != null) { 110 | if (msg.getCode() == 0 && msg.getData() != null) { 111 | if (!((ArrayList) msg.getData()).isEmpty()) { 112 | mBooks.addAll((ArrayList) msg.getData()); 113 | mAdapter.notifyDataSetChanged(); 114 | } 115 | } else if (mNowPage == 1) { 116 | Snackbar.make( 117 | mRecyclerView, 118 | R.string.tips_network_error, 119 | Snackbar.LENGTH_LONG 120 | ).setAction( 121 | R.string.snack_action_try_again, 122 | new View.OnClickListener() { 123 | @Override 124 | public void onClick(View view) { 125 | mSwipeRefreshLayout.setRefreshing(true); 126 | new PageGetTask().execute(mNowPage); 127 | } 128 | } 129 | ).show(); 130 | } 131 | } 132 | } 133 | 134 | } 135 | 136 | } 137 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/fragment/settings/SettingsLicense.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.fragment.settings; 2 | 3 | import android.app.Fragment; 4 | import android.os.Bundle; 5 | import android.view.LayoutInflater; 6 | import android.view.View; 7 | import android.view.ViewGroup; 8 | import android.webkit.WebView; 9 | 10 | import moe.feng.nhentai.R; 11 | 12 | public class SettingsLicense extends Fragment { 13 | 14 | @Override 15 | public View onCreateView(LayoutInflater inflater, ViewGroup group, Bundle bundle) { 16 | WebView webView = new WebView(inflater.getContext()); 17 | webView.loadUrl("file:///android_asset/licenses.html"); 18 | 19 | getActivity().setTitle(R.string.settings_license); 20 | 21 | return webView; 22 | } 23 | 24 | } 25 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/ui/fragment/settings/SettingsMain.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.ui.fragment.settings; 2 | 3 | import android.content.Intent; 4 | import android.net.Uri; 5 | import android.os.Bundle; 6 | import android.preference.PreferenceFragment; 7 | 8 | import moe.feng.nhentai.R; 9 | import moe.feng.nhentai.ui.SettingsActivity; 10 | import moe.feng.nhentai.view.pref.Preference; 11 | 12 | public class SettingsMain extends PreferenceFragment implements Preference.OnPreferenceClickListener { 13 | 14 | private Preference mVersionPref; 15 | private Preference mLicensePref; 16 | private Preference mWeiboPref; 17 | private Preference mGooglePlusPref; 18 | private Preference mGithubPref; 19 | 20 | @Override 21 | public void onCreate(Bundle savedInstanceState) { 22 | super.onCreate(savedInstanceState); 23 | addPreferencesFromResource(R.xml.settings_main); 24 | 25 | mVersionPref = (Preference) findPreference("version"); 26 | mLicensePref = (Preference) findPreference("license"); 27 | mWeiboPref = (Preference) findPreference("weibo"); 28 | mGooglePlusPref = (Preference) findPreference("google_plus"); 29 | mGithubPref = (Preference) findPreference("github"); 30 | 31 | String version = "Unknown"; 32 | try { 33 | version = getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionName; 34 | version += " (" + getActivity().getPackageManager().getPackageInfo(getActivity().getPackageName(), 0).versionCode + ")"; 35 | } catch (Exception e) { 36 | 37 | } 38 | mVersionPref.setSummary(version); 39 | 40 | mLicensePref.setOnPreferenceClickListener(this); 41 | mWeiboPref.setOnPreferenceClickListener(this); 42 | mGooglePlusPref.setOnPreferenceClickListener(this); 43 | mGithubPref.setOnPreferenceClickListener(this); 44 | } 45 | 46 | @Override 47 | public boolean onPreferenceClick(android.preference.Preference pref) { 48 | if (pref == mLicensePref) { 49 | SettingsActivity.launchActivity(getActivity(), SettingsActivity.FLAG_LICENSE); 50 | return true; 51 | } 52 | if (pref == mWeiboPref) { 53 | openWebUrl("http://weibo.com/fython"); 54 | return true; 55 | } 56 | if (pref == mGooglePlusPref) { 57 | openWebUrl("https://plus.google.com/+FungJichun"); 58 | return true; 59 | } 60 | if (pref == mGithubPref) { 61 | openWebUrl(getString(R.string.set_title_github_website)); 62 | } 63 | return false; 64 | } 65 | 66 | private void openWebUrl(String url) { 67 | Uri uri = Uri.parse(url); 68 | startActivity(new Intent(Intent.ACTION_VIEW, uri)); 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/util/AsyncTask.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.util; 2 | 3 | 4 | import android.os.Handler; 5 | import android.os.Message; 6 | import android.util.Log; 7 | 8 | import static moe.feng.nhentai.BuildConfig.DEBUG; 9 | 10 | /* 11 | Real AsyncTask 12 | */ 13 | 14 | public abstract class AsyncTask 15 | { 16 | private static final String TAG = AsyncTask.class.getSimpleName(); 17 | 18 | private static class AsyncResult { 19 | public AsyncTask task; 20 | public Data[] data; 21 | 22 | public AsyncResult(AsyncTask task, Data... data) { 23 | this.task = task; 24 | this.data = data; 25 | } 26 | } 27 | 28 | private static final int MSG_FINISH = 1000; 29 | private static final int MSG_PROGRESS = 1001; 30 | 31 | private static Handler sInternalHandler = new Handler() { 32 | @SuppressWarnings({"unchecked", "RawUseOfParameterizedType"}) 33 | @Override 34 | public void handleMessage(Message msg) { 35 | AsyncResult result = (AsyncResult) msg.obj; 36 | switch (msg.what) { 37 | case MSG_FINISH: 38 | result.task.onPostExecute(result.data[0]); 39 | break; 40 | case MSG_PROGRESS: 41 | result.task.onProgressUpdate(result.data); 42 | break; 43 | } 44 | } 45 | }; 46 | 47 | private Params[] mParams; 48 | 49 | private Thread mThread = new Thread(new Runnable() { 50 | @Override 51 | public void run() { 52 | // TODO: CrashHandler.register(); 53 | 54 | try { 55 | Result result = doInBackground(mParams); 56 | sInternalHandler.sendMessage(sInternalHandler.obtainMessage(MSG_FINISH, new AsyncResult(AsyncTask.this, result))); 57 | } catch (Exception e) { 58 | // Don't crash the whole app 59 | if (DEBUG) { 60 | Log.d(TAG, e.getClass().getSimpleName() + " caught when running background task. Printing stack trace."); 61 | Log.d(TAG, Log.getStackTraceString(e)); 62 | } 63 | } 64 | 65 | Thread.currentThread().interrupt(); 66 | } 67 | }); 68 | 69 | protected void onPostExecute(Result result) {} 70 | 71 | protected abstract Result doInBackground(Params... params); 72 | 73 | protected void onPreExecute() {} 74 | 75 | protected void onProgressUpdate(Progress... progress) {} 76 | 77 | protected void publishProgress(Progress... progress) { 78 | sInternalHandler.sendMessage(sInternalHandler.obtainMessage(MSG_PROGRESS, new AsyncResult(this, progress))); 79 | } 80 | 81 | public void execute(Params... params) { 82 | onPreExecute(); 83 | mParams = params; 84 | mThread.start(); 85 | } 86 | } 87 | 88 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/util/ColorGenerator.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.util; 2 | 3 | import java.util.Arrays; 4 | import java.util.List; 5 | import java.util.Random; 6 | 7 | /** 8 | * @author amulya 9 | * @datetime 14 Oct 2014, 5:20 PM 10 | */ 11 | public class ColorGenerator { 12 | 13 | public static ColorGenerator DEFAULT; 14 | 15 | public static ColorGenerator MATERIAL; 16 | 17 | static { 18 | DEFAULT = create(Arrays.asList( 19 | 0xfff16364, 20 | 0xfff58559, 21 | 0xfff9a43e, 22 | 0xffe4c62e, 23 | 0xff67bf74, 24 | 0xff59a2be, 25 | 0xff2093cd, 26 | 0xffad62a7, 27 | 0xff805781 28 | )); 29 | MATERIAL = create(Arrays.asList( 30 | 0xffe57373, 31 | 0xfff06292, 32 | 0xffba68c8, 33 | 0xff9575cd, 34 | 0xff7986cb, 35 | 0xff64b5f6, 36 | 0xff4fc3f7, 37 | 0xff4dd0e1, 38 | 0xff4db6ac, 39 | 0xff81c784, 40 | 0xffaed581, 41 | 0xffff8a65, 42 | 0xffd4e157, 43 | 0xffffd54f, 44 | 0xffffb74d, 45 | 0xffa1887f, 46 | 0xff90a4ae 47 | )); 48 | } 49 | 50 | private final List mColors; 51 | private final Random mRandom; 52 | 53 | public static ColorGenerator create(List colorList) { 54 | return new ColorGenerator(colorList); 55 | } 56 | 57 | private ColorGenerator(List colorList) { 58 | mColors = colorList; 59 | mRandom = new Random(System.currentTimeMillis()); 60 | } 61 | 62 | public int getRandomColor() { 63 | return mColors.get(mRandom.nextInt(mColors.size())); 64 | } 65 | 66 | public int getColor(Object key) { 67 | return mColors.get(Math.abs(key.hashCode()) % mColors.size()); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/util/FullScreenHelper.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.util; 2 | 3 | import android.annotation.SuppressLint; 4 | import android.app.Activity; 5 | import android.os.Build; 6 | import android.view.View; 7 | import android.view.ViewConfiguration; 8 | import android.view.Window; 9 | import android.view.WindowManager; 10 | 11 | @SuppressLint("InlinedApi") 12 | public final class FullScreenHelper implements View.OnSystemUiVisibilityChangeListener { 13 | 14 | private static final String TAG = FullScreenHelper.class.getSimpleName(); 15 | 16 | private final int NOT_FULL_SCREEN_JELLY_BEAN = 17 | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN | 18 | View.SYSTEM_UI_FLAG_LAYOUT_STABLE; 19 | 20 | private final int FULL_SCREEN_JELLY_BEAN = 21 | NOT_FULL_SCREEN_JELLY_BEAN | 22 | View.SYSTEM_UI_FLAG_LOW_PROFILE | 23 | View.SYSTEM_UI_FLAG_FULLSCREEN; 24 | 25 | private final int FULL_SCREEN_FLAG_JELLY_BEAN = 26 | View.SYSTEM_UI_FLAG_LOW_PROFILE | 27 | View.SYSTEM_UI_FLAG_FULLSCREEN; 28 | 29 | private final int NOT_FULL_SCREEN_KITKAT = 30 | NOT_FULL_SCREEN_JELLY_BEAN | 31 | View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION; 32 | 33 | private final int FULL_SCREEN_KITKAT = 34 | NOT_FULL_SCREEN_KITKAT | 35 | View.SYSTEM_UI_FLAG_LOW_PROFILE | 36 | View.SYSTEM_UI_FLAG_FULLSCREEN | 37 | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION | 38 | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; 39 | 40 | private boolean mFullScreen; 41 | private final Activity mActivity; 42 | private final Window mWindow; 43 | private final View mDecorView; 44 | 45 | private boolean mHasNavBar; 46 | 47 | private OnFullScreenBrokenListener mListener; 48 | 49 | private final int TEST_SDK = Build.VERSION.SDK_INT; 50 | 51 | public interface OnFullScreenBrokenListener { 52 | 53 | /** 54 | * FullScreen state should be fullScreen or not, 55 | * but user or system broke it 56 | * 57 | * @param fullScreen support to be 58 | */ 59 | public void onFullScreenBroken(boolean fullScreen); 60 | 61 | } 62 | 63 | public FullScreenHelper(Activity activity) { 64 | mFullScreen = false; 65 | mActivity = activity; 66 | mWindow = mActivity.getWindow(); 67 | mDecorView = mWindow.getDecorView(); 68 | mDecorView.setOnSystemUiVisibilityChangeListener(this); 69 | 70 | String mainKey = Utility.getSystemProperties("qemu.hw.mainkeys"); 71 | int resourceId = activity.getResources().getIdentifier("config_showNavigationBar", "bool", "android"); 72 | if (resourceId != 0) { 73 | mHasNavBar = activity.getResources().getBoolean(resourceId); 74 | // check override flag (see static block) 75 | if ("1".equals(mainKey)) { 76 | mHasNavBar = false; 77 | } else if ("0".equals(mainKey)) { 78 | mHasNavBar = true; 79 | } 80 | } else { 81 | mHasNavBar = !ViewConfiguration.get(activity).hasPermanentMenuKey(); 82 | } 83 | } 84 | 85 | public boolean willHideNavBar() { 86 | if (TEST_SDK >= Build.VERSION_CODES.KITKAT && mHasNavBar) 87 | return true; 88 | else 89 | return false; 90 | } 91 | 92 | @Override 93 | public void onSystemUiVisibilityChange(int visibility) { 94 | if (TEST_SDK >= Build.VERSION_CODES.JELLY_BEAN && 95 | TEST_SDK < Build.VERSION_CODES.KITKAT) { 96 | if ((mFullScreen && visibility != FULL_SCREEN_FLAG_JELLY_BEAN) || 97 | (!mFullScreen && visibility != 0)) { 98 | // User or system change visibility 99 | if (mListener != null) 100 | mListener.onFullScreenBroken(mFullScreen); 101 | } 102 | } 103 | } 104 | 105 | public void setOnFullScreenBrokenListener(OnFullScreenBrokenListener l) { 106 | mListener = l; 107 | } 108 | 109 | public boolean getFullScreen() { 110 | return mFullScreen; 111 | } 112 | 113 | public void setFullScreen(boolean fullScreen) { 114 | mFullScreen = fullScreen; 115 | if (fullScreen) { 116 | if (TEST_SDK < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 117 | // Empty 118 | } else if (TEST_SDK < Build.VERSION_CODES.JELLY_BEAN) { 119 | mWindow.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, 120 | WindowManager.LayoutParams.FLAG_FULLSCREEN); 121 | 122 | } else if (TEST_SDK < Build.VERSION_CODES.KITKAT) { 123 | mDecorView.setSystemUiVisibility(FULL_SCREEN_JELLY_BEAN); 124 | 125 | } else { 126 | mDecorView.setSystemUiVisibility(FULL_SCREEN_KITKAT); 127 | } 128 | } else { 129 | if (TEST_SDK < Build.VERSION_CODES.ICE_CREAM_SANDWICH) { 130 | // Empty 131 | } else if (TEST_SDK < Build.VERSION_CODES.JELLY_BEAN) { 132 | mWindow.clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN); 133 | 134 | } else if (TEST_SDK < Build.VERSION_CODES.KITKAT) { 135 | mDecorView.setSystemUiVisibility(NOT_FULL_SCREEN_JELLY_BEAN); 136 | 137 | } else { 138 | mDecorView.setSystemUiVisibility(NOT_FULL_SCREEN_KITKAT); 139 | } 140 | } 141 | } 142 | 143 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/util/HttpTools.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.util; 2 | 3 | import java.io.IOException; 4 | import java.net.HttpURLConnection; 5 | import java.net.URL; 6 | import java.net.URLEncoder; 7 | 8 | public class HttpTools { 9 | 10 | public static HttpURLConnection openConnection(String url) throws IOException { 11 | URL u = new URL(url); 12 | HttpURLConnection conn = (HttpURLConnection) u.openConnection(); 13 | conn.setConnectTimeout(5000); 14 | conn.setRequestMethod("GET"); 15 | conn.setRequestProperty("Accept-Encoding", "identity"); 16 | conn.setRequestProperty("Referer", URLEncoder.encode(url, "UTF-8")); 17 | conn.setRequestProperty("Charset", "UTF-8"); 18 | conn.setRequestProperty("Connection", "Keep-Alive"); 19 | return conn; 20 | } 21 | 22 | public static long getTargetContentSize(String url) throws IOException { 23 | HttpURLConnection conn = openConnection(url); 24 | if (conn.getResponseCode() == HttpURLConnection.HTTP_OK) { 25 | return conn.getContentLength(); 26 | } 27 | return -1; 28 | } 29 | 30 | } 31 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/util/Settings.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.util; 2 | 3 | import android.content.Context; 4 | 5 | import moe.feng.nhentai.dao.CommonPreferences; 6 | 7 | public class Settings { 8 | 9 | public static final String PREFERENCES_NAME = "settings"; 10 | 11 | private static Settings sInstance; 12 | 13 | private CommonPreferences mPrefs; 14 | 15 | public static Settings getInstance(Context context) { 16 | if (sInstance == null) { 17 | sInstance = new Settings(context); 18 | } 19 | return sInstance; 20 | } 21 | 22 | private Settings(Context context) { 23 | mPrefs = CommonPreferences.getInstance(context, PREFERENCES_NAME); 24 | } 25 | 26 | public Settings putBoolean(String key, boolean value) { 27 | mPrefs.edit().putBoolean(key, value).commit(); 28 | return this; 29 | } 30 | 31 | public boolean getBoolean(String key, boolean def) { 32 | return mPrefs.getBoolean(key, def); 33 | } 34 | 35 | public Settings putInt(String key, int value) { 36 | mPrefs.edit().putInt(key, value).commit(); 37 | return this; 38 | } 39 | 40 | public int getInt(String key, int defValue) { 41 | return mPrefs.getInt(key, defValue); 42 | } 43 | 44 | 45 | public Settings putString(String key, String value) { 46 | mPrefs.edit().putString(key, value).commit(); 47 | return this; 48 | } 49 | 50 | public String getString(String key, String defValue) { 51 | return mPrefs.getString(key, defValue); 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/util/Utility.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.util; 2 | 3 | import android.content.Context; 4 | import android.os.Build; 5 | 6 | import java.lang.reflect.Method; 7 | 8 | public class Utility { 9 | 10 | public static boolean isChrome() { 11 | return Build.BRAND.equals("chromium") || Build.BRAND.equals("chrome"); 12 | } 13 | 14 | public static int getStatusBarHeight(Context context) { 15 | int result = 0; 16 | int resourceId = context.getResources().getIdentifier("status_bar_height", "dimen", "android"); 17 | if (resourceId > 0) { 18 | result = context.getResources().getDimensionPixelSize(resourceId); 19 | } 20 | return result; 21 | } 22 | 23 | public static String getFirstCharacter(String sentence) { 24 | for (int i = 0; i < sentence.length(); i++) { 25 | String s = sentence.substring(i, i+1); 26 | if (s.equals("[") || s.equals("]")) continue; 27 | if (s.equals("{") || s.equals("}")) continue; 28 | if (s.equals("(") || s.equals(")")) continue; 29 | if (s.equals(",") || s.equals(".")) continue; 30 | if (s.equals("<") || s.equals(">")) continue; 31 | if (s.equals("《") || s.equals("》")) continue; 32 | if (s.equals("【") || s.equals("】")) continue; 33 | if (s.equals("{") || s.equals("}")) continue; 34 | return s; 35 | } 36 | return null; 37 | } 38 | 39 | public static String getSystemProperties(String key) { 40 | try { 41 | Class c = Class.forName("android.os.SystemProperties"); 42 | Method m = c.getDeclaredMethod("get", String.class); 43 | m.setAccessible(true); 44 | return (String) m.invoke(null, key); 45 | } catch (Throwable e) { 46 | return ""; 47 | } 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/view/ExpandableHeightGridView.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.view; 2 | 3 | import android.content.Context; 4 | import android.util.AttributeSet; 5 | import android.view.ViewGroup; 6 | import android.widget.GridView; 7 | 8 | public class ExpandableHeightGridView extends GridView { 9 | 10 | private boolean expanded = false; 11 | 12 | public ExpandableHeightGridView(Context context) { 13 | super(context); 14 | } 15 | 16 | public ExpandableHeightGridView(Context context, AttributeSet attrs) { 17 | super(context, attrs); 18 | } 19 | 20 | public ExpandableHeightGridView(Context context, AttributeSet attrs, int defStyle) { 21 | super(context, attrs, defStyle); 22 | } 23 | 24 | public boolean isExpanded() { 25 | return expanded; 26 | } 27 | 28 | @Override 29 | public void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { 30 | if (isExpanded()) { 31 | int expandSpec = MeasureSpec.makeMeasureSpec( 32 | Integer.MAX_VALUE >> 2, MeasureSpec.AT_MOST); 33 | super.onMeasure(widthMeasureSpec, expandSpec); 34 | 35 | ViewGroup.LayoutParams params = getLayoutParams(); 36 | params.height = getMeasuredHeight(); 37 | } else { 38 | super.onMeasure(widthMeasureSpec, heightMeasureSpec); 39 | } 40 | } 41 | 42 | public void setExpanded(boolean expanded) { 43 | this.expanded = expanded; 44 | } 45 | 46 | } -------------------------------------------------------------------------------- /app/src/main/java/moe/feng/nhentai/view/pref/Preference.java: -------------------------------------------------------------------------------- 1 | package moe.feng.nhentai.view.pref; 2 | 3 | import android.annotation.TargetApi; 4 | import android.content.Context; 5 | import android.os.Build.VERSION_CODES; 6 | import android.util.AttributeSet; 7 | 8 | import moe.feng.nhentai.R; 9 | 10 | public class Preference extends android.preference.Preference { 11 | 12 | private boolean _isInitialized = false; 13 | 14 | protected void init(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 15 | setLayoutResource(R.layout.custom_preference); 16 | } 17 | 18 | @TargetApi(VERSION_CODES.LOLLIPOP) 19 | public Preference(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) { 20 | super(context, attrs, defStyleAttr, defStyleRes); 21 | if (!_isInitialized) { 22 | _isInitialized = true; 23 | init(context, attrs, defStyleAttr, defStyleRes); 24 | } 25 | } 26 | 27 | public Preference(Context context, AttributeSet attrs, int defStyleAttr) { 28 | super(context, attrs, defStyleAttr); 29 | if (!_isInitialized) { 30 | _isInitialized = true; 31 | init(context, attrs, defStyleAttr, 0); 32 | } 33 | } 34 | 35 | public Preference(Context context, AttributeSet attrs) { 36 | super(context, attrs); 37 | if (!_isInitialized) { 38 | _isInitialized = true; 39 | init(context, attrs, 0, 0); 40 | } 41 | } 42 | 43 | public Preference(Context context) { 44 | super(context); 45 | if (!_isInitialized) { 46 | _isInitialized = true; 47 | init(context, null, 0, 0); 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/DataProcessor.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv; 10 | 11 | import org.json.JSONArray; 12 | import org.json.JSONException; 13 | import org.json.JSONObject; 14 | 15 | public class DataProcessor 16 | { 17 | public static class Persistable 18 | { 19 | public static Object dePrefix(String k) throws JSONException 20 | { 21 | if (k.startsWith("String_")) 22 | { 23 | return k.substring("String_".length()); 24 | } 25 | else if (k.startsWith("Boolean_")) 26 | { 27 | return Boolean.parseBoolean(k.substring("Boolean_".length())); 28 | } 29 | else if (k.startsWith("Integer_")) 30 | { 31 | return Integer.parseInt(k.substring("Integer_".length())); 32 | } 33 | else if (k.startsWith("Float_")) 34 | { 35 | return Float.parseFloat(k.substring("Float_".length())); 36 | } 37 | else if (k.startsWith("Double_")) 38 | { 39 | return Double.parseDouble(k.substring("Double_".length())); 40 | } 41 | else if (k.startsWith("Long_")) 42 | { 43 | return Long.parseLong(k.substring("Long_".length())); 44 | } 45 | else if (k.startsWith("JSONArray_")) 46 | { 47 | return new JSONArray(k.substring("JSONArray_".length())); 48 | } 49 | else if (k.startsWith("JSONObject_")) 50 | { 51 | return new JSONObject(k.substring("JSONObject_".length())); 52 | } 53 | else 54 | { 55 | return null; 56 | } 57 | } 58 | 59 | public static boolean isValidDataType(Object obj) 60 | { 61 | if (obj instanceof String 62 | || obj instanceof Integer 63 | || obj instanceof Boolean 64 | || obj instanceof Long 65 | || obj instanceof Float 66 | || obj instanceof Double 67 | || obj instanceof JSONObject 68 | || obj instanceof JSONArray) 69 | { 70 | return true; 71 | } 72 | else 73 | { 74 | return false; 75 | } 76 | } 77 | 78 | public static String addPrefix(Object obj) 79 | { 80 | if (obj instanceof String) 81 | { 82 | return "String_" + obj.toString(); 83 | } 84 | else if (obj instanceof Integer) 85 | { 86 | return "Integer_" + obj.toString(); 87 | } 88 | else if (obj instanceof Boolean) 89 | { 90 | return "Boolean_" + obj.toString(); 91 | } 92 | else if (obj instanceof Long) 93 | { 94 | return "Long_" + obj.toString(); 95 | } 96 | else if (obj instanceof Float) 97 | { 98 | return "Float_" + obj.toString(); 99 | } 100 | else if (obj instanceof Double) 101 | { 102 | return "Double_" + obj.toString(); 103 | } 104 | else if (obj instanceof org.json.JSONObject) 105 | { 106 | return "JSONObject_" + obj.toString(); 107 | } 108 | else if (obj instanceof org.json.JSONArray) 109 | { 110 | return "JSONArray_" + obj.toString(); 111 | } 112 | else 113 | { 114 | return obj.toString(); 115 | } 116 | } 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/QKVConfig.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv; 10 | 11 | public class QKVConfig 12 | { 13 | public static final boolean DEBUG = false; //Change it to true in development mode. 14 | public static final String PUBLIC_LTAG = "QuickKV"; //Default log tag. There is no need to change it. 15 | public static final String KVDB_FILE_NAME = "database.qkv"; //Default KVDB filename 16 | public static final String KVDB_NAME = "database"; //Name (must match above) 17 | public static final String KVDB_EXT = ".qkv"; //Ext (must match above above) 18 | public static final String EC_PREFIX = "__QKVEC_"; // :-/ Abandoned 19 | } 20 | -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/QKVFSReader.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv; 10 | 11 | import java.io.BufferedReader; 12 | import java.io.FileNotFoundException; 13 | import java.io.FileReader; 14 | import java.io.IOException; 15 | import java.io.RandomAccessFile; 16 | import java.io.StringReader; 17 | import java.nio.ByteBuffer; 18 | import java.nio.CharBuffer; 19 | import java.nio.channels.FileChannel; 20 | import java.nio.charset.Charset; 21 | 22 | public class QKVFSReader 23 | { 24 | public static String readFileBFD(String pFileAbsPath) throws IOException { 25 | return bfd(pFileAbsPath); 26 | } 27 | 28 | public static String readFileNIO(String pFileAbsPath) throws IOException{ 29 | return nio(pFileAbsPath); 30 | } 31 | 32 | private static String bfd(String pFilePath) throws FileNotFoundException,IOException{ 33 | BufferedReader bufferedReader = new BufferedReader(new FileReader(pFilePath)); 34 | StringBuilder sb = new StringBuilder(); 35 | String str = null; 36 | while((str = bufferedReader.readLine()) != null){ 37 | sb.append(str); 38 | } 39 | return sb.toString(); 40 | } 41 | 42 | private static String nio(String pFilePath) throws FileNotFoundException,IOException{ 43 | RandomAccessFile file = new RandomAccessFile(pFilePath, "r"); 44 | FileChannel fileChannel = file.getChannel(); 45 | ByteBuffer buffer = ByteBuffer.allocateDirect((int) fileChannel.size()); 46 | fileChannel.read(buffer); 47 | buffer.flip(); 48 | CharBuffer charBuffer = Charset.forName("utf-8").decode(buffer); 49 | file.close(); 50 | BufferedReader bufferedReader = new BufferedReader(new StringReader(charBuffer.toString())); 51 | StringBuilder sb = new StringBuilder(); 52 | String str = null; 53 | while((str = bufferedReader.readLine()) != null){ 54 | sb.append(str); 55 | } 56 | return sb.toString(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/QKVLogger.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv; 10 | 11 | import android.util.Log; 12 | 13 | public class QKVLogger 14 | { 15 | public static void log(String level, String msg) 16 | { 17 | if (QKVConfig.DEBUG) 18 | { 19 | if (level.equals("i")) 20 | { 21 | Log.i(QKVConfig.PUBLIC_LTAG, msg); 22 | } 23 | else if (level.equals("w")) 24 | { 25 | Log.w(QKVConfig.PUBLIC_LTAG, msg); 26 | } 27 | } 28 | } 29 | public static void ex(Exception e) 30 | { 31 | if (QKVConfig.DEBUG) 32 | { 33 | e.printStackTrace(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/QuickKV.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv; 10 | 11 | import android.content.Context; 12 | 13 | import java.util.HashMap; 14 | 15 | import sumimakito.android.quickkv.database.KeyValueDatabase; 16 | 17 | public class QuickKV 18 | { 19 | private Context pContext; 20 | private HashMap sKVDB; 21 | 22 | public QuickKV(Context context) 23 | { 24 | this.pContext = context; 25 | this.sKVDB = new HashMap(); 26 | } 27 | 28 | public KeyValueDatabase getDatabase() 29 | { 30 | if (!this.sKVDB.containsKey(QKVConfig.KVDB_FILE_NAME)) 31 | { 32 | this.sKVDB.put(QKVConfig.KVDB_FILE_NAME, new KeyValueDatabase(pContext)); 33 | } 34 | return this.sKVDB.get(QKVConfig.KVDB_FILE_NAME); 35 | } 36 | 37 | public KeyValueDatabase getDatabase(String dbAlias) 38 | { 39 | if (dbAlias.equals(QKVConfig.KVDB_NAME) || dbAlias.equals(QKVConfig.KVDB_FILE_NAME)) 40 | { 41 | return getDatabase(); 42 | } 43 | if (dbAlias == null) 44 | { 45 | return null; 46 | } 47 | else 48 | { 49 | if (dbAlias.length() == 0) 50 | { 51 | dbAlias = QKVConfig.KVDB_FILE_NAME; 52 | } 53 | dbAlias = dbAlias.endsWith(QKVConfig.KVDB_EXT) ?dbAlias: dbAlias + QKVConfig.KVDB_EXT; 54 | } 55 | if (!this.sKVDB.containsKey(dbAlias)) 56 | { 57 | this.sKVDB.put(dbAlias, new KeyValueDatabase(pContext, dbAlias)); 58 | } 59 | 60 | return this.sKVDB.get(dbAlias); 61 | } 62 | 63 | public KeyValueDatabase getDatabase(String dbAlias, String key) 64 | { 65 | if (dbAlias.equals(QKVConfig.KVDB_NAME) || dbAlias.equals(QKVConfig.KVDB_FILE_NAME)) 66 | { 67 | return getDatabase(); 68 | } 69 | if (dbAlias == null) 70 | { 71 | return null; 72 | } 73 | else 74 | { 75 | if (dbAlias.length() == 0) 76 | { 77 | dbAlias = QKVConfig.KVDB_FILE_NAME; 78 | } 79 | dbAlias = dbAlias.endsWith(QKVConfig.KVDB_EXT) ?dbAlias: dbAlias + QKVConfig.KVDB_EXT; 80 | } 81 | if (!this.sKVDB.containsKey(dbAlias)) 82 | { 83 | this.sKVDB.put(dbAlias, new KeyValueDatabase(pContext, dbAlias, key)); 84 | } 85 | 86 | return this.sKVDB.get(dbAlias); 87 | } 88 | 89 | public boolean isDatabaseOpened() 90 | { 91 | return this.sKVDB.containsKey(QKVConfig.KVDB_FILE_NAME); 92 | } 93 | 94 | public boolean isDatabaseOpened(String dbAlias) 95 | { 96 | if (dbAlias.equals(QKVConfig.KVDB_NAME) || dbAlias.equals(QKVConfig.KVDB_FILE_NAME)) 97 | { 98 | return isDatabaseOpened(); 99 | } 100 | if (dbAlias == null) 101 | { 102 | return false; 103 | } 104 | else 105 | { 106 | if (dbAlias.length() == 0) 107 | { 108 | dbAlias = QKVConfig.KVDB_FILE_NAME; 109 | } 110 | dbAlias = dbAlias.endsWith(QKVConfig.KVDB_EXT) ?dbAlias: dbAlias + QKVConfig.KVDB_EXT; 111 | } 112 | return this.sKVDB.containsKey(dbAlias); 113 | } 114 | 115 | public boolean releaseDatabase() 116 | { 117 | if (isDatabaseOpened()) 118 | { 119 | this.sKVDB.remove(QKVConfig.KVDB_FILE_NAME); 120 | return true; 121 | } 122 | return false; 123 | } 124 | 125 | public boolean releaseDatabase(String dbAlias) 126 | { 127 | if (isDatabaseOpened(dbAlias)) 128 | { 129 | dbAlias = dbAlias.endsWith(QKVConfig.KVDB_EXT) ?dbAlias: dbAlias + QKVConfig.KVDB_EXT; 130 | this.sKVDB.remove(dbAlias); 131 | return true; 132 | } 133 | return false; 134 | } 135 | 136 | public void releaseAllDatabases() 137 | { 138 | this.sKVDB.clear(); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/database/QKVDatabase.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv.database; 10 | 11 | public interface QKVDatabase 12 | { 13 | boolean put(K k, V v); 14 | Object get(K k); 15 | boolean containsKey(K k); 16 | boolean containsValue(V v); 17 | boolean remove(K k); 18 | boolean remove(K[] k); 19 | void clear(); 20 | int size(); 21 | } 22 | -------------------------------------------------------------------------------- /app/src/main/java/sumimakito/android/quickkv/security/AES256.java: -------------------------------------------------------------------------------- 1 | /** 2 | * QucikKV 3 | * Copyright (c) 2014-2015 Sumi Makito 4 | * Licensed under Apache License 2.0. 5 | * @author sumimakito 6 | * @version 0.8.2 7 | */ 8 | 9 | package sumimakito.android.quickkv.security; 10 | 11 | import android.util.Base64; 12 | 13 | import java.io.UnsupportedEncodingException; 14 | import java.util.Arrays; 15 | 16 | import javax.crypto.Cipher; 17 | import javax.crypto.SecretKey; 18 | import javax.crypto.spec.IvParameterSpec; 19 | import javax.crypto.spec.SecretKeySpec; 20 | 21 | import moe.feng.nhentai.BuildConfig; 22 | import sumimakito.android.quickkv.QKVConfig; 23 | 24 | public class AES256 25 | { 26 | public static String encode(String ePasK, String eConT){ 27 | if (ePasK.length() == 0 || ePasK == null) { 28 | return ""; 29 | } 30 | 31 | if (eConT.length() == 0 || eConT == null) { 32 | return ""; 33 | } 34 | 35 | try { 36 | SecretKeySpec skeySpec = getKey(ePasK); 37 | byte[] clearText = eConT.getBytes("UTF8"); 38 | 39 | final byte[] iv = new byte[16]; 40 | Arrays.fill(iv, (byte) 0x00); 41 | IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); 42 | 43 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); 44 | cipher.init(Cipher.ENCRYPT_MODE, skeySpec, ivParameterSpec); 45 | 46 | String encrypedValue = Base64.encodeToString(cipher.doFinal(clearText), Base64.DEFAULT); 47 | return encrypedValue; 48 | 49 | } catch (Exception e) { 50 | if (QKVConfig.DEBUG){ 51 | e.printStackTrace(); 52 | } 53 | } 54 | return ""; 55 | } 56 | 57 | public static String decode(String ePasK, String eConT){ 58 | if (ePasK.length() == 0 || ePasK == null) { 59 | return ""; 60 | } 61 | if (eConT.length() == 0 || eConT == null) { 62 | return ""; 63 | } 64 | try { 65 | SecretKey key = getKey(ePasK); 66 | final byte[] iv = new byte[16]; 67 | Arrays.fill(iv, (byte) 0x00); 68 | IvParameterSpec ivParameterSpec = new IvParameterSpec(iv); 69 | byte[] encrypedPwdBytes = Base64.decode(eConT, Base64.DEFAULT); 70 | Cipher cipher = Cipher.getInstance("AES/CBC/PKCS7Padding"); 71 | cipher.init(Cipher.DECRYPT_MODE, key, ivParameterSpec); 72 | byte[] decrypedValueBytes = (cipher.doFinal(encrypedPwdBytes)); 73 | 74 | String decrypedValue = new String(decrypedValueBytes); 75 | return decrypedValue; 76 | 77 | } catch (Exception e) { 78 | if (BuildConfig.DEBUG){ 79 | e.printStackTrace(); 80 | } 81 | } 82 | return ""; 83 | } 84 | private static SecretKeySpec getKey(String password) throws UnsupportedEncodingException { 85 | int keyLength = 256; 86 | byte[] keyBytes = new byte[keyLength / 8]; 87 | Arrays.fill(keyBytes, (byte) 0x0); 88 | byte[] passwordBytes = password.getBytes("UTF-8"); 89 | int length = passwordBytes.length < keyBytes.length ? passwordBytes.length : keyBytes.length; 90 | System.arraycopy(passwordBytes, 0, keyBytes, 0, length); 91 | SecretKeySpec key = new SecretKeySpec(keyBytes, "AES"); 92 | return key; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /app/src/main/res/color/drawer_item_color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_face_unlock_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-hdpi/ic_face_unlock_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-hdpi/ic_history.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-hdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_photo_library_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-hdpi/ic_photo_library_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-hdpi/ic_style_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-hdpi/ic_style_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_face_unlock_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xhdpi/ic_face_unlock_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xhdpi/ic_history.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xhdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_photo_library_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xhdpi/ic_photo_library_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xhdpi/ic_style_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xhdpi/ic_style_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_face_unlock_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xxhdpi/ic_face_unlock_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_history.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xxhdpi/ic_history.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xxhdpi/ic_home_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_photo_library_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xxhdpi/ic_photo_library_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxhdpi/ic_style_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xxhdpi/ic_style_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable-xxxhdpi/ic_photo_library_black_24dp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable-xxxhdpi/ic_photo_library_black_24dp.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/holder_0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable/holder_0.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/holder_1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable/holder_1.png -------------------------------------------------------------------------------- /app/src/main/res/drawable/holder_2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/drawable/holder_2.jpg -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_gradient.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_gradient_reserve.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_normal.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/drawable/shadow_normal_reserve.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 9 | 10 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_book_details.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 15 | 16 | 25 | 26 | 33 | 34 | 40 | 41 | 47 | 48 | 49 | 50 | 51 | 52 | 60 | 61 | 68 | 69 | 78 | 79 | 85 | 86 | 91 | 92 | 99 | 100 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 123 | 124 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_gallery.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 12 | 13 | 19 | 20 | 24 | 25 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_main.xml: -------------------------------------------------------------------------------- 1 | 7 | 8 | 12 | 13 | 18 | 19 | 26 | 27 | 33 | 34 | 35 | 36 | 41 | 42 | 43 | 44 | 49 | 50 | 59 | 60 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_search_result.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 19 | 20 | 24 | 25 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /app/src/main/res/layout/activity_settings.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 12 | 13 | 19 | 20 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_preference.xml: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | 24 | 30 | 31 | 32 | 42 | 43 | 44 | 45 | 46 | 54 | 55 | -------------------------------------------------------------------------------- /app/src/main/res/layout/custom_preference_widget_switch.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_book_page.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 11 | 12 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_download.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_favorite.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | -------------------------------------------------------------------------------- /app/src/main/res/layout/fragment_home.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 10 | 11 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_book_card.xml: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 19 | 20 | 27 | 28 | 36 | 37 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_book_picture_thumb.xml: -------------------------------------------------------------------------------- 1 | 2 | 11 | 12 | 17 | 18 | 24 | 25 | -------------------------------------------------------------------------------- /app/src/main/res/layout/list_item_menu_row.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/layout/navigation_header.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | -------------------------------------------------------------------------------- /app/src/main/res/menu/menu_main.xml: -------------------------------------------------------------------------------- 1 | 5 | 6 | 11 | 12 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/menu/navigation_menu.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 10 | 11 | 14 | 15 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app/src/main/res/values-v21/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 10 | 11 | 17 | 18 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rCN/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NH 本子 5 | 设置 6 | 搜索 7 | 8 | 主页 9 | 标签 10 | 人物 11 | 12 | 最新 13 | 已保存的本子 14 | 我的收藏 15 | 16 | @string/title_page_latest 17 | @string/title_page_download 18 | @string/title_page_favorite 19 | 20 | 无法连接到 NHentai。 21 | 重试 22 | 23 | 详情 24 | 标签: 25 | 作品: 26 | 语言: 27 | 人物: 28 | 画家: 29 | 组别: 30 | 上传时间: 31 | 32 | 图库 33 | 34 | 搜索结果 35 | 搜索结果: %s 36 | 没有结果。 37 | 38 | 设置 39 | 关于 40 | 应用程序版本 41 | 开源项目许可协议 42 | 新浪微博 43 | \@某燒餅 44 | Google+ 45 | +Fung Jichun 46 | Github 开源项目 47 | "https://github.com/fython/NHentai-android" 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values-zh-rTW/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NH 本子 5 | 設置 6 | 搜尋 7 | 8 | 主頁 9 | 標籤 10 | 角色 11 | 12 | 最新 13 | 已儲存的本子 14 | 我的最愛 15 | 16 | @string/title_page_latest 17 | @string/title_page_download 18 | @string/title_page_favorite 19 | 20 | 無法連接到 NHentai。 21 | 重試 22 | 23 | 詳情 24 | 標籤: 25 | 作品: 26 | 語言: 27 | 角色: 28 | 畫家: 29 | 組別: 30 | 上載時間: 31 | 32 | 畫廊 33 | 34 | 搜尋結果 35 | 搜尋結果: %s 36 | 沒有結果。 37 | 38 | 設置 39 | 關於 40 | 應用程式版本 41 | 開源項目許可協議 42 | 新浪微博 43 | \@某燒餅 44 | Google+ 45 | +Fung Jichun 46 | Github 開源項目 47 | "https://github.com/fython/NHentai-android" 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values/attrs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /app/src/main/res/values/color.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | #9575CD 5 | #673AB7 6 | #4527A0 7 | 8 | #BA68C8 9 | #E91E63 10 | #3F51B5 11 | #2196F3 12 | #009688 13 | #4CAF50 14 | #81C784 15 | #FF9800 16 | #795548 17 | 18 | #4F000000 19 | 20 | -------------------------------------------------------------------------------- /app/src/main/res/values/dimen.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5dp 5 | 6 | 256dp 7 | 8 | 320dp 9 | 10 | 2dp 11 | 4dp 12 | 14sp 13 | 72sp 14 | 15 | -------------------------------------------------------------------------------- /app/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | NHBooks 5 | Settings 6 | Search 7 | 8 | Home 9 | Tags 10 | Characters 11 | 12 | Latest 13 | Saved books 14 | Favorites 15 | 16 | @string/title_page_latest 17 | @string/title_page_download 18 | @string/title_page_favorite 19 | 20 | Failed to connect to NHentai. 21 | Try again 22 | 23 | Details 24 | Tags: 25 | Parodies: 26 | Language: 27 | Characters: 28 | Artists: 29 | Groups: 30 | Uploaded time: 31 | 32 | Gallery 33 | 34 | Search result 35 | Search result: %s 36 | No result. 37 | 38 | Settings 39 | About 40 | Application version 41 | Licenses 42 | Sina Weibo 43 | \@某燒餅 44 | Google Plus 45 | +Fung Jichun 46 | Github Repo 47 | "https://github.com/fython/NHentai-android" 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 7 | 8 | 12 | 13 | 22 | 23 | 26 | 27 | 36 | 37 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /app/src/main/res/xml/settings_main.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 9 | 10 | 13 | 14 | 18 | 19 | 23 | 24 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /art/nhbooks.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/art/nhbooks.png -------------------------------------------------------------------------------- /art/screenshot0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/art/screenshot0.png -------------------------------------------------------------------------------- /art/screenshot1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/art/screenshot1.png -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | // Top-level build file where you can add configuration options common to all sub-projects/modules. 2 | 3 | buildscript { 4 | repositories { 5 | jcenter() 6 | } 7 | dependencies { 8 | classpath 'com.android.tools.build:gradle:1.1.0' 9 | 10 | // NOTE: Do not place your application dependencies here; they belong 11 | // in the individual module build.gradle files 12 | } 13 | } 14 | 15 | allprojects { 16 | repositories { 17 | jcenter() 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Project-wide Gradle settings. 2 | 3 | # IDE (e.g. Android Studio) users: 4 | # Gradle settings configured through the IDE *will override* 5 | # any settings specified in this file. 6 | 7 | # For more details on how to configure your build environment visit 8 | # http://www.gradle.org/docs/current/userguide/build_environment.html 9 | 10 | # Specifies the JVM arguments used for the daemon process. 11 | # The setting is particularly useful for tweaking memory settings. 12 | # Default value: -Xmx10248m -XX:MaxPermSize=256m 13 | # org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 14 | 15 | # When configured, Gradle will run in incubating parallel mode. 16 | # This option should only be used with decoupled projects. More details, visit 17 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects 18 | # org.gradle.parallel=true -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Wed May 27 13:24:38 CST 2015 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-2.2-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 10 | DEFAULT_JVM_OPTS="" 11 | 12 | APP_NAME="Gradle" 13 | APP_BASE_NAME=`basename "$0"` 14 | 15 | # Use the maximum available, or set MAX_FD != -1 to use that value. 16 | MAX_FD="maximum" 17 | 18 | warn ( ) { 19 | echo "$*" 20 | } 21 | 22 | die ( ) { 23 | echo 24 | echo "$*" 25 | echo 26 | exit 1 27 | } 28 | 29 | # OS specific support (must be 'true' or 'false'). 30 | cygwin=false 31 | msys=false 32 | darwin=false 33 | case "`uname`" in 34 | CYGWIN* ) 35 | cygwin=true 36 | ;; 37 | Darwin* ) 38 | darwin=true 39 | ;; 40 | MINGW* ) 41 | msys=true 42 | ;; 43 | esac 44 | 45 | # For Cygwin, ensure paths are in UNIX format before anything is touched. 46 | if $cygwin ; then 47 | [ -n "$JAVA_HOME" ] && JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 48 | fi 49 | 50 | # Attempt to set APP_HOME 51 | # Resolve links: $0 may be a link 52 | PRG="$0" 53 | # Need this for relative symlinks. 54 | while [ -h "$PRG" ] ; do 55 | ls=`ls -ld "$PRG"` 56 | link=`expr "$ls" : '.*-> \(.*\)$'` 57 | if expr "$link" : '/.*' > /dev/null; then 58 | PRG="$link" 59 | else 60 | PRG=`dirname "$PRG"`"/$link" 61 | fi 62 | done 63 | SAVED="`pwd`" 64 | cd "`dirname \"$PRG\"`/" >&- 65 | APP_HOME="`pwd -P`" 66 | cd "$SAVED" >&- 67 | 68 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 69 | 70 | # Determine the Java command to use to start the JVM. 71 | if [ -n "$JAVA_HOME" ] ; then 72 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 73 | # IBM's JDK on AIX uses strange locations for the executables 74 | JAVACMD="$JAVA_HOME/jre/sh/java" 75 | else 76 | JAVACMD="$JAVA_HOME/bin/java" 77 | fi 78 | if [ ! -x "$JAVACMD" ] ; then 79 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 80 | 81 | Please set the JAVA_HOME variable in your environment to match the 82 | location of your Java installation." 83 | fi 84 | else 85 | JAVACMD="java" 86 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 87 | 88 | Please set the JAVA_HOME variable in your environment to match the 89 | location of your Java installation." 90 | fi 91 | 92 | # Increase the maximum file descriptors if we can. 93 | if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then 94 | MAX_FD_LIMIT=`ulimit -H -n` 95 | if [ $? -eq 0 ] ; then 96 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 97 | MAX_FD="$MAX_FD_LIMIT" 98 | fi 99 | ulimit -n $MAX_FD 100 | if [ $? -ne 0 ] ; then 101 | warn "Could not set maximum file descriptor limit: $MAX_FD" 102 | fi 103 | else 104 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 105 | fi 106 | fi 107 | 108 | # For Darwin, add options to specify how the application appears in the dock 109 | if $darwin; then 110 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 111 | fi 112 | 113 | # For Cygwin, switch paths to Windows format before running java 114 | if $cygwin ; then 115 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 116 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules 158 | function splitJvmOpts() { 159 | JVM_OPTS=("$@") 160 | } 161 | eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS 162 | JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" 163 | 164 | exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" 165 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 12 | set DEFAULT_JVM_OPTS= 13 | 14 | set DIRNAME=%~dp0 15 | if "%DIRNAME%" == "" set DIRNAME=. 16 | set APP_BASE_NAME=%~n0 17 | set APP_HOME=%DIRNAME% 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windowz variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | if "%@eval[2+2]" == "4" goto 4NT_args 53 | 54 | :win9xME_args 55 | @rem Slurp the command line arguments. 56 | set CMD_LINE_ARGS= 57 | set _SKIP=2 58 | 59 | :win9xME_args_slurp 60 | if "x%~1" == "x" goto execute 61 | 62 | set CMD_LINE_ARGS=%* 63 | goto execute 64 | 65 | :4NT_args 66 | @rem Get arguments from the 4NT Shell from JP Software 67 | set CMD_LINE_ARGS=%$ 68 | 69 | :execute 70 | @rem Setup the command line 71 | 72 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 73 | 74 | @rem Execute Gradle 75 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 76 | 77 | :end 78 | @rem End local scope for the variables with windows NT shell 79 | if "%ERRORLEVEL%"=="0" goto mainEnd 80 | 81 | :fail 82 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 83 | rem the _cmd.exe /c_ return code! 84 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 85 | exit /b 1 86 | 87 | :mainEnd 88 | if "%OS%"=="Windows_NT" endlocal 89 | 90 | :omega 91 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/.gitignore: -------------------------------------------------------------------------------- 1 | /build 2 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'com.android.library' 2 | 3 | android { 4 | compileSdkVersion 22 5 | buildToolsVersion "22.0.1" 6 | 7 | defaultConfig { 8 | minSdkVersion 11 9 | targetSdkVersion 22 10 | versionCode 1 11 | versionName "1.0" 12 | } 13 | buildTypes { 14 | release { 15 | minifyEnabled false 16 | proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' 17 | } 18 | } 19 | lintOptions { 20 | abortOnError false 21 | } 22 | 23 | } 24 | 25 | dependencies { 26 | compile fileTree(dir: 'libs', include: ['*.jar']) 27 | compile 'com.nineoldandroids:library:2.4.0+' 28 | compile 'com.android.support:appcompat-v7:22.2.0' 29 | } 30 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/gradle.properties: -------------------------------------------------------------------------------- 1 | POM_NAME=Persistent Search 2 | POM_ARTIFACT_ID=library 3 | POM_PACKAGING=aar -------------------------------------------------------------------------------- /libraries/PersistentSearch/proguard-rules.pro: -------------------------------------------------------------------------------- 1 | # Add project specific ProGuard rules here. 2 | # By default, the flags in this file are appended to flags specified 3 | # in C:/Users/Kieron/Android/adt-bundle-windows-x86_64-20140702/adt-bundle-windows-x86_64-20140702/sdk/tools/proguard/proguard-android.txt 4 | # You can edit the include path and order by changing the proguardFiles 5 | # directive in build.gradle. 6 | # 7 | # For more details, see 8 | # http://developer.android.com/guide/developing/tools/proguard.html 9 | 10 | # Add any project specific keep options here: 11 | 12 | # If your project uses WebView with JS, uncomment the following 13 | # and specify the fully qualified class name to the JavaScript interface 14 | # class: 15 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview { 16 | # public *; 17 | #} 18 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/com/balysv/materialmenu/MaterialMenu.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2014 Balys Valentukevicius 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package com.balysv.materialmenu; 18 | 19 | import android.view.animation.Interpolator; 20 | 21 | import com.nineoldandroids.animation.Animator; 22 | 23 | import static com.balysv.materialmenu.MaterialMenuDrawable.IconState; 24 | 25 | /** 26 | * API for interaction with {@link MaterialMenuDrawable} 27 | */ 28 | public interface MaterialMenu { 29 | /** 30 | * Change icon without animation 31 | * 32 | * @param state new icon state 33 | */ 34 | public void setState(IconState state); 35 | 36 | /** 37 | * Return current icon state 38 | * 39 | * @return icon state 40 | */ 41 | public IconState getState(); 42 | 43 | /** 44 | * Animate icon to given state. 45 | * 46 | * @param state new icon state 47 | */ 48 | public void animateState(IconState state); 49 | 50 | /** 51 | * Animate icon to given state and draw touch circle 52 | * 53 | * @param state new icon state 54 | */ 55 | public void animatePressedState(IconState state); 56 | 57 | /** 58 | * Set color of icon 59 | * 60 | * @param color new icon color 61 | */ 62 | public void setColor(int color); 63 | 64 | /** 65 | * Set duration of transformation animations 66 | * 67 | * @param duration new animation duration 68 | */ 69 | public void setTransformationDuration(int duration); 70 | 71 | /** 72 | * Set duration of pressed state circle animation 73 | * 74 | * @param duration new animation duration 75 | */ 76 | public void setPressedDuration(int duration); 77 | 78 | /** 79 | * Set interpolator for transformation animations 80 | * 81 | * @param interpolator new interpolator 82 | */ 83 | public void setInterpolator(Interpolator interpolator); 84 | 85 | /** 86 | * Set listener for {@code MaterialMenuDrawable} animation events 87 | * 88 | * @param listener new listener or null to remove any listener 89 | */ 90 | public void setAnimationListener(Animator.AnimatorListener listener); 91 | 92 | /** 93 | * Enable RTL layout. Flips all icons horizontally 94 | * 95 | * @param rtlEnabled true to enable RTL layout 96 | */ 97 | public void setRTLEnabled(boolean rtlEnabled); 98 | 99 | /** 100 | * Manually set a transformation value for an {@link com.balysv.materialmenu.MaterialMenuDrawable.AnimationState} 101 | * 102 | * @param animationState state to set value in 103 | * @param value between {@link com.balysv.materialmenu.MaterialMenuDrawable#TRANSFORMATION_START} and 104 | * {@link com.balysv.materialmenu.MaterialMenuDrawable#TRANSFORMATION_END}. 105 | */ 106 | public void setTransformationOffset(MaterialMenuDrawable.AnimationState animationState, float value); 107 | 108 | /** 109 | * @return {@link MaterialMenuDrawable} to be used for the menu 110 | */ 111 | public MaterialMenuDrawable getDrawable(); 112 | } 113 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/com/quinny898/library/persistentsearch/SearchResult.java: -------------------------------------------------------------------------------- 1 | package com.quinny898.library.persistentsearch; 2 | 3 | import android.graphics.drawable.Drawable; 4 | import android.support.annotation.DrawableRes; 5 | 6 | public class SearchResult { 7 | 8 | public String title; 9 | public Drawable icon; 10 | public int drawableResId; 11 | 12 | /** 13 | * Create a search result with text and an icon 14 | * @param title 15 | * @param icon 16 | */ 17 | public SearchResult(String title, Drawable icon) { 18 | this.title = title; 19 | this.icon = icon; 20 | } 21 | 22 | public SearchResult(String title, @DrawableRes int drawableResId) { 23 | this.title = title; 24 | this.drawableResId = drawableResId; 25 | } 26 | 27 | /** 28 | * Return the title of the result 29 | */ 30 | @Override 31 | public String toString() { 32 | return title; 33 | } 34 | 35 | } -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/animation/RevealAnimator.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Rect; 5 | import android.os.Build; 6 | import android.view.View; 7 | 8 | import com.nineoldandroids.animation.Animator; 9 | 10 | import java.lang.ref.WeakReference; 11 | 12 | import static io.codetail.animation.ViewAnimationUtils.SimpleAnimationListener; 13 | 14 | /** 15 | * @hide 16 | */ 17 | public interface RevealAnimator{ 18 | 19 | public void setClipOutlines(boolean clip); 20 | 21 | public void setCenter(float cx, float cy); 22 | 23 | public void setTarget(View target); 24 | 25 | public void setRevealRadius(float value); 26 | 27 | public float getRevealRadius(); 28 | 29 | public void invalidate(Rect bounds); 30 | 31 | static class RevealFinishedGingerbread extends SimpleAnimationListener { 32 | WeakReference mReference; 33 | volatile Rect mInvalidateBounds; 34 | 35 | RevealFinishedGingerbread(RevealAnimator target, Rect bounds) { 36 | mReference = new WeakReference<>(target); 37 | mInvalidateBounds = bounds; 38 | } 39 | 40 | @Override 41 | public void onAnimationEnd(Animator animation) { 42 | super.onAnimationEnd(animation); 43 | 44 | RevealAnimator target = mReference.get(); 45 | 46 | if(target == null){ 47 | return; 48 | } 49 | 50 | target.setClipOutlines(false); 51 | target.setCenter(0, 0); 52 | target.setTarget(null); 53 | target.invalidate(mInvalidateBounds); 54 | } 55 | } 56 | 57 | static class RevealFinishedIceCreamSandwich extends SimpleAnimationListener { 58 | WeakReference mReference; 59 | volatile Rect mInvalidateBounds; 60 | 61 | int mLayerType; 62 | 63 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 64 | RevealFinishedIceCreamSandwich(RevealAnimator target, Rect bounds) { 65 | mReference = new WeakReference<>(target); 66 | mInvalidateBounds = bounds; 67 | 68 | mLayerType = ((View) target).getLayerType(); 69 | } 70 | 71 | @Override 72 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 73 | public void onAnimationStart(Animator animation) { 74 | super.onAnimationStart(animation); 75 | ((View) mReference.get()).setLayerType(View.LAYER_TYPE_SOFTWARE, null); 76 | } 77 | 78 | @Override 79 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 80 | public void onAnimationEnd(Animator animation) { 81 | super.onAnimationEnd(animation); 82 | ((View) mReference.get()).setLayerType(mLayerType, null); 83 | 84 | RevealAnimator target = mReference.get(); 85 | 86 | if(target == null){ 87 | return; 88 | } 89 | 90 | target.setClipOutlines(false); 91 | target.setCenter(0, 0); 92 | target.setTarget(null); 93 | target.invalidate(mInvalidateBounds); 94 | } 95 | } 96 | 97 | static class RevealFinishedJellyBeanMr1 extends SimpleAnimationListener { 98 | WeakReference mReference; 99 | volatile Rect mInvalidateBounds; 100 | 101 | int mLayerType; 102 | 103 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 104 | RevealFinishedJellyBeanMr1(RevealAnimator target, Rect bounds) { 105 | mReference = new WeakReference<>(target); 106 | mInvalidateBounds = bounds; 107 | 108 | mLayerType = ((View) target).getLayerType(); 109 | } 110 | 111 | @Override 112 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 113 | public void onAnimationStart(Animator animation) { 114 | super.onAnimationStart(animation); 115 | ((View) mReference.get()).setLayerType(View.LAYER_TYPE_HARDWARE, null); 116 | } 117 | 118 | @Override 119 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 120 | public void onAnimationEnd(Animator animation) { 121 | super.onAnimationEnd(animation); 122 | ((View) mReference.get()).setLayerType(mLayerType, null); 123 | 124 | RevealAnimator target = mReference.get(); 125 | 126 | if(target == null){ 127 | return; 128 | } 129 | 130 | target.setClipOutlines(false); 131 | target.setCenter(0, 0); 132 | target.setTarget(null); 133 | target.invalidate(mInvalidateBounds); 134 | } 135 | } 136 | } -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/animation/ReverseInterpolator.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.view.animation.Interpolator; 4 | 5 | public class ReverseInterpolator implements Interpolator { 6 | 7 | public float getInterpolation(float t) { 8 | t = Math.abs(t -1f); 9 | float x = t*2.0f; 10 | if (t<0.5f) return 0.5f*x*x*x*x*x; 11 | x = (t-0.5f)*2-1; 12 | return 0.5f*x*x*x*x*x+1; 13 | } 14 | } -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/animation/SupportAnimator.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.view.animation.Interpolator; 4 | 5 | public abstract class SupportAnimator { 6 | 7 | /** 8 | * @return true if using native android animation framework, otherwise is 9 | * nineoldandroids 10 | */ 11 | public abstract boolean isNativeAnimator(); 12 | 13 | /** 14 | * @return depends from {@link android.os.Build.VERSION} if sdk version 15 | * {@link android.os.Build.VERSION_CODES#LOLLIPOP} and greater will return 16 | * {@link android.animation.Animator} otherwise {@link com.nineoldandroids.animation.Animator} 17 | */ 18 | public abstract Object get(); 19 | 20 | /** 21 | * Starts this animation. If the animation has a nonzero startDelay, the animation will start 22 | * running after that delay elapses. A non-delayed animation will have its initial 23 | * value(s) set immediately, followed by calls to 24 | * {@link android.animation.Animator.AnimatorListener#onAnimationStart(android.animation.Animator)} 25 | * for any listeners of this animator. 26 | * 27 | *

The animation started by calling this method will be run on the thread that called 28 | * this method. This thread should have a Looper on it (a runtime exception will be thrown if 29 | * this is not the case). Also, if the animation will animate 30 | * properties of objects in the view hierarchy, then the calling thread should be the UI 31 | * thread for that view hierarchy.

32 | * 33 | */ 34 | public abstract void start(); 35 | 36 | /** 37 | * Sets the duration of the animation. 38 | * 39 | * @param duration The length of the animation, in milliseconds. 40 | */ 41 | public abstract void setDuration(int duration); 42 | 43 | /** 44 | * The time interpolator used in calculating the elapsed fraction of the 45 | * animation. The interpolator determines whether the animation runs with 46 | * linear or non-linear motion, such as acceleration and deceleration. The 47 | * default value is {@link android.view.animation.AccelerateDecelerateInterpolator}. 48 | * 49 | * @param value the interpolator to be used by this animation 50 | */ 51 | public abstract void setInterpolator(Interpolator value); 52 | 53 | 54 | /** 55 | * Adds a listener to the set of listeners that are sent events through the life of an 56 | * animation, such as start, repeat, and end. 57 | * 58 | * @param listener the listener to be added to the current set of listeners for this animation. 59 | */ 60 | public abstract void addListener(AnimatorListener listener); 61 | 62 | 63 | /** 64 | * Returns whether this Animator is currently running (having been started and gone past any 65 | * initial startDelay period and not yet ended). 66 | * 67 | * @return Whether the Animator is running. 68 | */ 69 | public abstract boolean isRunning(); 70 | 71 | 72 | /** 73 | *

An animation listener receives notifications from an animation. 74 | * Notifications indicate animation related events, such as the end or the 75 | * repetition of the animation.

76 | */ 77 | public static interface AnimatorListener { 78 | /** 79 | *

Notifies the start of the animation.

80 | */ 81 | void onAnimationStart(); 82 | 83 | /** 84 | *

Notifies the end of the animation. This callback is not invoked 85 | * for animations with repeat count set to INFINITE.

86 | */ 87 | void onAnimationEnd(); 88 | 89 | /** 90 | *

Notifies the cancellation of the animation. This callback is not invoked 91 | * for animations with repeat count set to INFINITE.

92 | */ 93 | void onAnimationCancel(); 94 | 95 | /** 96 | *

Notifies the repetition of the animation.

97 | */ 98 | void onAnimationRepeat(); 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/animation/SupportAnimatorLollipop.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.animation.Animator; 4 | import android.annotation.TargetApi; 5 | import android.os.Build; 6 | import android.view.animation.Interpolator; 7 | 8 | import java.lang.ref.WeakReference; 9 | 10 | @TargetApi(Build.VERSION_CODES.HONEYCOMB) 11 | final class SupportAnimatorLollipop extends SupportAnimator{ 12 | 13 | WeakReference mNativeAnimator; 14 | 15 | SupportAnimatorLollipop(Animator animator) { 16 | mNativeAnimator = new WeakReference(animator); 17 | } 18 | 19 | @Override 20 | public boolean isNativeAnimator() { 21 | return true; 22 | } 23 | 24 | @Override 25 | public Object get() { 26 | return mNativeAnimator; 27 | } 28 | 29 | 30 | @Override 31 | public void start() { 32 | Animator a = mNativeAnimator.get(); 33 | if(a != null) { 34 | a.start(); 35 | } 36 | } 37 | 38 | @Override 39 | public void setDuration(int duration) { 40 | Animator a = mNativeAnimator.get(); 41 | if(a != null) { 42 | a.setDuration(duration); 43 | } 44 | } 45 | 46 | @Override 47 | public void setInterpolator(Interpolator value) { 48 | Animator a = mNativeAnimator.get(); 49 | if(a != null) { 50 | a.setInterpolator(value); 51 | } 52 | } 53 | 54 | @Override 55 | public void addListener(final AnimatorListener listener) { 56 | Animator a = mNativeAnimator.get(); 57 | if(a == null) { 58 | return; 59 | } 60 | 61 | if(listener == null){ 62 | a.addListener(null); 63 | return; 64 | } 65 | 66 | a.addListener(new Animator.AnimatorListener() { 67 | @Override 68 | public void onAnimationStart(Animator animation) { 69 | listener.onAnimationStart(); 70 | } 71 | 72 | @Override 73 | public void onAnimationEnd(Animator animation) { 74 | listener.onAnimationEnd(); 75 | } 76 | 77 | @Override 78 | public void onAnimationCancel(Animator animation) { 79 | listener.onAnimationCancel(); 80 | } 81 | 82 | @Override 83 | public void onAnimationRepeat(Animator animation) { 84 | listener.onAnimationRepeat(); 85 | } 86 | }); 87 | } 88 | 89 | @Override 90 | public boolean isRunning() { 91 | Animator a = mNativeAnimator.get(); 92 | return a != null && a.isRunning(); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/animation/SupportAnimatorPreL.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.view.animation.Interpolator; 4 | 5 | import com.nineoldandroids.animation.Animator; 6 | 7 | import java.lang.ref.WeakReference; 8 | 9 | final class SupportAnimatorPreL extends SupportAnimator { 10 | 11 | WeakReference mSupportFramework; 12 | 13 | SupportAnimatorPreL(Animator animator) { 14 | mSupportFramework = new WeakReference(animator); 15 | } 16 | 17 | @Override 18 | public boolean isNativeAnimator() { 19 | return false; 20 | } 21 | 22 | @Override 23 | public Object get() { 24 | return mSupportFramework.get(); 25 | } 26 | 27 | @Override 28 | public void start() { 29 | Animator a = mSupportFramework.get(); 30 | if(a != null) { 31 | a.start(); 32 | } 33 | } 34 | 35 | @Override 36 | public void setDuration(int duration) { 37 | Animator a = mSupportFramework.get(); 38 | if(a != null) { 39 | a.setDuration(duration); 40 | } 41 | } 42 | 43 | @Override 44 | public void setInterpolator(Interpolator value) { 45 | Animator a = mSupportFramework.get(); 46 | if(a != null) { 47 | a.setInterpolator(value); 48 | } 49 | } 50 | 51 | @Override 52 | public void addListener(final AnimatorListener listener) { 53 | Animator a = mSupportFramework.get(); 54 | if(a == null) { 55 | return; 56 | } 57 | 58 | if(listener == null){ 59 | a.addListener(null); 60 | return; 61 | } 62 | 63 | a.addListener(new Animator.AnimatorListener() { 64 | @Override 65 | public void onAnimationStart(Animator animation) { 66 | listener.onAnimationStart(); 67 | } 68 | 69 | @Override 70 | public void onAnimationEnd(Animator animation) { 71 | listener.onAnimationEnd(); 72 | } 73 | 74 | @Override 75 | public void onAnimationCancel(Animator animation) { 76 | listener.onAnimationCancel(); 77 | } 78 | 79 | @Override 80 | public void onAnimationRepeat(Animator animation) { 81 | listener.onAnimationRepeat(); 82 | } 83 | }); 84 | } 85 | 86 | @Override 87 | public boolean isRunning() { 88 | Animator a = mSupportFramework.get(); 89 | return a != null && a.isRunning(); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/animation/ViewAnimationUtils.java: -------------------------------------------------------------------------------- 1 | package io.codetail.animation; 2 | 3 | import android.annotation.TargetApi; 4 | import android.graphics.Rect; 5 | import android.os.Build; 6 | import android.view.View; 7 | import android.view.animation.AccelerateDecelerateInterpolator; 8 | 9 | import com.nineoldandroids.animation.Animator; 10 | import com.nineoldandroids.animation.ObjectAnimator; 11 | import com.nineoldandroids.view.ViewHelper; 12 | import com.nineoldandroids.view.ViewPropertyAnimator; 13 | 14 | import static android.os.Build.VERSION.SDK_INT; 15 | import static android.os.Build.VERSION_CODES.LOLLIPOP; 16 | 17 | public class ViewAnimationUtils { 18 | 19 | private final static boolean LOLLIPOP_PLUS = SDK_INT >= LOLLIPOP; 20 | 21 | public static final int SCALE_UP_DURATION = 500; 22 | 23 | /** 24 | * Returns an Animator which can animate a clipping circle. 25 | *

26 | * Any shadow cast by the View will respect the circular clip from this animator. 27 | *

28 | * Only a single non-rectangular clip can be applied on a View at any time. 29 | * Views clipped by a circular reveal animation take priority over 30 | * {@link android.view.View#setClipToOutline(boolean) View Outline clipping}. 31 | *

32 | * Note that the animation returned here is a one-shot animation. It cannot 33 | * be re-used, and once started it cannot be paused or resumed. 34 | * 35 | * @param view The View will be clipped to the animating circle. 36 | * @param centerX The x coordinate of the center of the animating circle. 37 | * @param centerY The y coordinate of the center of the animating circle. 38 | * @param startRadius The starting radius of the animating circle. 39 | * @param endRadius The ending radius of the animating circle. 40 | */ 41 | @TargetApi(Build.VERSION_CODES.LOLLIPOP) 42 | public static SupportAnimator createCircularReveal(View view, 43 | int centerX, int centerY, 44 | float startRadius, float endRadius) { 45 | 46 | if(LOLLIPOP_PLUS){ 47 | return new SupportAnimatorLollipop(android.view.ViewAnimationUtils 48 | .createCircularReveal(view, centerX, centerY, startRadius, endRadius)); 49 | } 50 | 51 | if(!(view.getParent() instanceof RevealAnimator)){ 52 | throw new IllegalArgumentException("View must be inside RevealFrameLayout or RevealLinearLayout."); 53 | } 54 | 55 | RevealAnimator revealLayout = (RevealAnimator) view.getParent(); 56 | revealLayout.setTarget(view); 57 | revealLayout.setCenter(centerX, centerY); 58 | 59 | Rect bounds = new Rect(); 60 | view.getHitRect(bounds); 61 | 62 | ObjectAnimator reveal = ObjectAnimator.ofFloat(revealLayout, "revealRadius", startRadius, endRadius); 63 | reveal.addListener(getRevealFinishListener(revealLayout, bounds)); 64 | 65 | return new SupportAnimatorPreL(reveal); 66 | } 67 | 68 | 69 | static Animator.AnimatorListener getRevealFinishListener(RevealAnimator target, Rect bounds){ 70 | if(SDK_INT >= 17){ 71 | return new RevealAnimator.RevealFinishedJellyBeanMr1(target, bounds); 72 | }else if(SDK_INT >= 14){ 73 | return new RevealAnimator.RevealFinishedIceCreamSandwich(target, bounds); 74 | }else { 75 | return new RevealAnimator.RevealFinishedGingerbread(target, bounds); 76 | } 77 | } 78 | 79 | 80 | /** 81 | * Lifting view 82 | * 83 | * @param view The animation target 84 | * @param baseRotation initial Rotation X in 3D space 85 | * @param fromY initial Y position of view 86 | * @param duration aniamtion duration 87 | * @param startDelay start delay before animation begin 88 | */ 89 | public static void liftingFromBottom(View view, float baseRotation, float fromY, int duration, int startDelay){ 90 | ViewHelper.setRotationX(view, baseRotation); 91 | ViewHelper.setTranslationY(view, fromY); 92 | 93 | ViewPropertyAnimator 94 | .animate(view) 95 | .setInterpolator(new AccelerateDecelerateInterpolator()) 96 | .setDuration(duration) 97 | .setStartDelay(startDelay) 98 | .rotationX(0) 99 | .translationY(0) 100 | .start(); 101 | 102 | } 103 | 104 | /** 105 | * Lifting view 106 | * 107 | * @param view The animation target 108 | * @param baseRotation initial Rotation X in 3D space 109 | * @param duration aniamtion duration 110 | * @param startDelay start delay before animation begin 111 | */ 112 | public static void liftingFromBottom(View view, float baseRotation, int duration, int startDelay){ 113 | ViewHelper.setRotationX(view, baseRotation); 114 | ViewHelper.setTranslationY(view, view.getHeight() / 3); 115 | 116 | ViewPropertyAnimator 117 | .animate(view) 118 | .setInterpolator(new AccelerateDecelerateInterpolator()) 119 | .setDuration(duration) 120 | .setStartDelay(startDelay) 121 | .rotationX(0) 122 | .translationY(0) 123 | .start(); 124 | 125 | } 126 | 127 | /** 128 | * Lifting view 129 | * 130 | * @param view The animation target 131 | * @param baseRotation initial Rotation X in 3D space 132 | * @param duration aniamtion duration 133 | */ 134 | public static void liftingFromBottom(View view, float baseRotation, int duration){ 135 | ViewHelper.setRotationX(view, baseRotation); 136 | ViewHelper.setTranslationY(view, view.getHeight() / 3); 137 | 138 | ViewPropertyAnimator 139 | .animate(view) 140 | .setInterpolator(new AccelerateDecelerateInterpolator()) 141 | .setDuration(duration) 142 | .rotationX(0) 143 | .translationY(0) 144 | .start(); 145 | 146 | } 147 | 148 | public static class SimpleAnimationListener implements Animator.AnimatorListener{ 149 | 150 | @Override 151 | public void onAnimationStart(Animator animation) { 152 | 153 | } 154 | 155 | @Override 156 | public void onAnimationEnd(Animator animation) { 157 | 158 | } 159 | 160 | @Override 161 | public void onAnimationCancel(Animator animation) { 162 | 163 | } 164 | 165 | @Override 166 | public void onAnimationRepeat(Animator animation) { 167 | 168 | } 169 | } 170 | 171 | } 172 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/widget/RevealFrameLayout.java: -------------------------------------------------------------------------------- 1 | package io.codetail.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.FrameLayout; 9 | 10 | import io.codetail.animation.RevealAnimator; 11 | 12 | public class RevealFrameLayout extends FrameLayout implements RevealAnimator{ 13 | 14 | Path mRevealPath; 15 | 16 | boolean mClipOutlines; 17 | 18 | float mCenterX; 19 | float mCenterY; 20 | float mRadius; 21 | 22 | View mTarget; 23 | 24 | public RevealFrameLayout(Context context) { 25 | this(context, null); 26 | } 27 | 28 | public RevealFrameLayout(Context context, AttributeSet attrs) { 29 | this(context, attrs, 0); 30 | } 31 | 32 | public RevealFrameLayout(Context context, AttributeSet attrs, int defStyle) { 33 | super(context, attrs, defStyle); 34 | mRevealPath = new Path(); 35 | } 36 | 37 | /** 38 | * Animation target 39 | * 40 | * @hide 41 | */ 42 | @Override 43 | public void setTarget(View view){ 44 | mTarget = view; 45 | } 46 | 47 | /** 48 | * Epicenter of animation circle reveal 49 | * 50 | * @hide 51 | */ 52 | @Override 53 | public void setCenter(float centerX, float centerY){ 54 | mCenterX = centerX; 55 | mCenterY = centerY; 56 | } 57 | 58 | /** 59 | * Flag that animation is enabled 60 | * 61 | * @hide 62 | */ 63 | @Override 64 | public void setClipOutlines(boolean clip){ 65 | mClipOutlines = clip; 66 | } 67 | 68 | /** 69 | * Circle radius size 70 | * 71 | * @hide 72 | */ 73 | @Override 74 | public void setRevealRadius(float radius){ 75 | mRadius = radius; 76 | invalidate(); 77 | } 78 | 79 | /** 80 | * Circle radius size 81 | * 82 | * @hide 83 | */ 84 | @Override 85 | public float getRevealRadius(){ 86 | return mRadius; 87 | } 88 | 89 | 90 | @Override 91 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 92 | if(!mClipOutlines && child != mTarget) 93 | return super.drawChild(canvas, child, drawingTime); 94 | 95 | final int state = canvas.save(); 96 | 97 | mRevealPath.reset(); 98 | mRevealPath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW); 99 | 100 | canvas.clipPath(mRevealPath); 101 | 102 | boolean isInvalided = super.drawChild(canvas, child, drawingTime); 103 | 104 | canvas.restoreToCount(state); 105 | 106 | return isInvalided; 107 | } 108 | 109 | } 110 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/java/io/codetail/widget/RevealLinearLayout.java: -------------------------------------------------------------------------------- 1 | package io.codetail.widget; 2 | 3 | import android.content.Context; 4 | import android.graphics.Canvas; 5 | import android.graphics.Path; 6 | import android.util.AttributeSet; 7 | import android.view.View; 8 | import android.widget.LinearLayout; 9 | 10 | import io.codetail.animation.RevealAnimator; 11 | 12 | public class RevealLinearLayout extends LinearLayout implements RevealAnimator{ 13 | 14 | Path mRevealPath; 15 | 16 | boolean mClipOutlines; 17 | 18 | float mCenterX; 19 | float mCenterY; 20 | float mRadius; 21 | 22 | View mTarget; 23 | 24 | public RevealLinearLayout(Context context) { 25 | this(context, null); 26 | } 27 | 28 | public RevealLinearLayout(Context context, AttributeSet attrs) { 29 | this(context, attrs, 0); 30 | } 31 | 32 | public RevealLinearLayout(Context context, AttributeSet attrs, int defStyle) { 33 | super(context, attrs); 34 | mRevealPath = new Path(); 35 | } 36 | 37 | /** 38 | * @hide 39 | */ 40 | @Override 41 | public void setTarget(View view){ 42 | mTarget = view; 43 | } 44 | 45 | /** 46 | * @hide 47 | */ 48 | @Override 49 | public void setCenter(float centerX, float centerY){ 50 | mCenterX = centerX; 51 | mCenterY = centerY; 52 | } 53 | 54 | /** 55 | * @hide 56 | */ 57 | @Override 58 | public void setClipOutlines(boolean clip){ 59 | mClipOutlines = clip; 60 | } 61 | 62 | /** 63 | * @hide 64 | */ 65 | @Override 66 | public void setRevealRadius(float radius){ 67 | mRadius = radius; 68 | invalidate(); 69 | } 70 | 71 | /** 72 | * @hide 73 | */ 74 | @Override 75 | public float getRevealRadius(){ 76 | return mRadius; 77 | } 78 | 79 | 80 | @Override 81 | protected boolean drawChild(Canvas canvas, View child, long drawingTime) { 82 | if (!mClipOutlines && child != mTarget) 83 | return super.drawChild(canvas, child, drawingTime); 84 | 85 | final int state = canvas.save(); 86 | 87 | mRevealPath.reset(); 88 | mRevealPath.addCircle(mCenterX, mCenterY, mRadius, Path.Direction.CW); 89 | 90 | canvas.clipPath(mRevealPath); 91 | 92 | boolean isInvalided = super.drawChild(canvas, child, drawingTime); 93 | 94 | canvas.restoreToCount(state); 95 | 96 | return isInvalided; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/anim/anim_down.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-hdpi/ic_action_mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-hdpi/ic_action_mic.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-hdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-hdpi/ic_clear.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-hdpi/ic_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-hdpi/ic_up.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-hdpi/search_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-hdpi/search_bg.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-mdpi/ic_action_mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-mdpi/ic_action_mic.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-mdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-mdpi/ic_clear.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-mdpi/ic_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-mdpi/ic_up.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-mdpi/search_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-mdpi/search_bg.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xhdpi/ic_action_mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xhdpi/ic_action_mic.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xhdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xhdpi/ic_clear.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xhdpi/ic_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xhdpi/ic_up.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xhdpi/search_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xhdpi/search_bg.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/ic_action_mic.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/ic_action_mic.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/ic_clear.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/ic_clear.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/ic_up.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/ic_up.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_bg.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_bg.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_bg_shadow.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_bg_shadow.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_bg_transparent.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_bg_transparent.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_frame.9.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NoireFHC/NHentai-android/140f9ae647994423f3df6447f7f45fa05c5e1ef9/libraries/PersistentSearch/src/main/res/drawable-xxhdpi/search_frame.9.png -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/layout/search_option.xml: -------------------------------------------------------------------------------- 1 | 2 | 9 | 10 | 14 | 15 | 24 | 25 | 36 | 37 | 46 | 47 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/layout/searchbox.xml: -------------------------------------------------------------------------------- 1 | 2 | 7 | 17 | 18 | 24 | 25 | 26 | 32 | 33 | 47 | 48 | 62 | 63 | 64 | 65 | 66 | 77 | 78 | 86 | 87 | 97 | 98 | 105 | 106 | 107 | 108 | 109 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/values/strings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | Speak now 4 | 5 | 6 | -------------------------------------------------------------------------------- /libraries/PersistentSearch/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app', ':libraries:PersistentSearch' 2 | --------------------------------------------------------------------------------