commit a511d42ecd32ba3994b35fdfe456a630a57f6db6 Author: bernd32 Date: Mon Dec 16 00:14:48 2019 +0500 Initial commit diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dfe0770 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,2 @@ +# Auto detect text files and perform LF normalization +* text=auto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..603b140 --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx diff --git a/.idea/.name b/.idea/.name new file mode 100644 index 0000000..0153353 --- /dev/null +++ b/.idea/.name @@ -0,0 +1 @@ +JLyrics \ No newline at end of file diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..fb22c1d --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,164 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + xmlns:android + + ^$ + + + +
+
+ + + + xmlns:.* + + ^$ + + + BY_NAME + +
+
+ + + + .*:id + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + .*:name + + http://schemas.android.com/apk/res/android + + + +
+
+ + + + name + + ^$ + + + +
+
+ + + + style + + ^$ + + + +
+
+ + + + .* + + ^$ + + + BY_NAME + +
+
+ + + + .* + + http://schemas.android.com/apk/res/android + + + ANDROID_ATTRIBUTE_ORDER + +
+
+ + + + .* + + .* + + + BY_NAME + +
+
+
+
+
+
\ No newline at end of file diff --git a/.idea/dbnavigator.xml b/.idea/dbnavigator.xml new file mode 100644 index 0000000..1962184 --- /dev/null +++ b/.idea/dbnavigator.xml @@ -0,0 +1,454 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..d291b3d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,16 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..b6ea2b1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/JLyrics.zip b/JLyrics.zip new file mode 100644 index 0000000..f3983b4 Binary files /dev/null and b/JLyrics.zip differ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..b09cd78 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ +Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md new file mode 100644 index 0000000..733cf54 --- /dev/null +++ b/README.md @@ -0,0 +1,2 @@ +# jlyrics-android + An unoffical android client of j-lyrics.net diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..bceedd2 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,85 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +apply plugin: 'com.android.application' + +android { + android { + // Kuromoji + packagingOptions { + exclude 'META-INF/CONTRIBUTORS.md' + exclude 'META-INF/LICENSE.md' + exclude 'META-INF/MANIFEST.MF' + exclude 'META-INF/NOTICE.md' + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + } + compileSdkVersion 29 + buildToolsVersion "29.0.2" + defaultConfig { + applicationId "com.bernd32.jlyrics" + minSdkVersion 23 + targetSdkVersion 29 + versionCode 1 + versionName "1.0" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + testImplementation 'junit:junit:4.12' + androidTestImplementation 'androidx.test:runner:1.2.0' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' + //Jsoup + implementation 'org.jsoup:jsoup:1.12.1' + // RecyclerView + implementation 'androidx.recyclerview:recyclerview:1.1.0' + implementation 'androidx.recyclerview:recyclerview:1.1.0' + // Glide + implementation 'com.github.bumptech.glide:glide:4.10.0' + annotationProcessor 'com.github.bumptech.glide:glide:4.10.0' + // Material + implementation 'com.google.android.material:material:1.2.0-alpha02' + + implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' + + implementation 'androidx.paging:paging-runtime:2.1.0' + // Room components + implementation "androidx.room:room-runtime:$rootProject.roomVersion" + annotationProcessor "androidx.room:room-compiler:$rootProject.roomVersion" + androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion" + // Lifecycle components + implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion" + annotationProcessor "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion" + // Junit + implementation 'androidx.test.ext:junit:1.1.2-alpha02' + // debugImplementation because LeakCanary should only run in debug builds. + debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.0' + // Romaji-henkan + implementation 'io.github.bernd32:romaji-henkan:0.0.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/app/roadmap/todo.txt b/app/roadmap/todo.txt new file mode 100644 index 0000000..822ce93 --- /dev/null +++ b/app/roadmap/todo.txt @@ -0,0 +1,62 @@ +1. Implement detailed search + ✓1.1 Prepare UI + ✓1.1.1 In MainActivity add spinner (song name, artist, words from lyrics) + ✓1.1.2 Add "Detailed search button" + ✓1.1.3 Show hidden UI elements: song name, artist and lyrics EditText and spinner (full, partial ) + ✓1.2 If user entered only artist, show artist search result + ✓1.2.1 Modify SearchActivity.documentHandle() + ✓1.3 Implement artist page + +2. UI changes + ✓2.1 Add Magnifying-glass icon on the search button + ✓2.2 Add FAB button which scrolls the page all the way up, and which appears after user scrolled 2nd page + ✓2.3 Change app's color theme + 2.4 Add an additional search button (at the bottom of detailed search form) + +✓3. Handle a no results situation + +4. Add options menu + ✓5.1 Enable dark theme + ✓5.2 Quit app + ✓5.3 Contact developer + ✓5.4 Add/Remove to favorites + ✓5.5 Copy lyrics + ✓5.6 Change font size (dialog) + ✓5.7 Open favorites + ✓5.7.1 Create favorites activity + +5. Fix lyrics page + ✓4.1 Add toolbar + ✓4.2 Make page scrollable + ✓4.3 Scale font size of textview with pinch zoom (https://stackoverflow.com/questions/40203241/android-custom-shape-for-zoom-buttons) + ✓4.4 Add option in toolbar to translate lyrics to romaji + ✓4.5 Show ProgressBar when loading lyrics + ✓4.6 Hide toolbar when scrolled + ✓4.7 Finish making share button + +✓6. Create "Add lyrics to favorites" feature (https://codelabs.developers.google.com/codelabs/android-room-with-a-view/) + ✓6.1 Delete lyric by swiping + ✓6.2 Delete lyric by long tap + ✓6.3 In LyricsView, detect that if current lyrics is in the database + +7. Catch all possible exceptions + +8. Miscellaneous + ✓8.1 Fix the progress bar bug: from artist-list to song + ✓8.2 Add auto-increment ID field into the sql table + ✓8.3 Add app icon + ✓8.4 Change default picture in CardView + ✓8.5 Optimize the code + ✓8.5.1 Get rid of ButterKnife dependence + ✓8.6 Create random user-agent generator + ✓8.7 Adapt app's color theme in WebView + +9. Convert lyrics to romaji + ✓9.1 Install kuromoji dependence + ✓9.2 Adapt jakaroma library + ✓9.3 Execute romanize() in AsyncTask + ✓9.4 Switch to original + ✓9.5 Hide layout and show progressBar and cancel fab + +✓ - completed + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..2aa66f3 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png new file mode 100644 index 0000000..5ba5760 Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ diff --git a/app/src/main/java/com/bernd32/jlyrics/BaseViewHolder.java b/app/src/main/java/com/bernd32/jlyrics/BaseViewHolder.java new file mode 100644 index 0000000..eac9ede --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/BaseViewHolder.java @@ -0,0 +1,43 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics; + +import android.view.View; + +import androidx.recyclerview.widget.RecyclerView; + +public abstract class BaseViewHolder extends RecyclerView.ViewHolder { + + private int mCurrentPosition; + + protected BaseViewHolder(View itemView) { + super(itemView); + } + + protected abstract void clear(); + + public void onBind(int position) { + mCurrentPosition = position; + clear(); + } + + public int getCurrentPosition() { + return mCurrentPosition; + } + +} + diff --git a/app/src/main/java/com/bernd32/jlyrics/Lyric.java b/app/src/main/java/com/bernd32/jlyrics/Lyric.java new file mode 100644 index 0000000..a1e5dc7 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/Lyric.java @@ -0,0 +1,75 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics; + +public class Lyric { + + private String cardTitle; + private String cardDescription; + private String imgUrl; + private String songUrl; + private String artistUrl; + private String url; + + public String getArtistUrl() { + return artistUrl; + } + + public void setArtistUrl(String artistUrl) { + this.artistUrl = artistUrl; + } + + public String getCardTitle() { + return cardTitle; + } + + public void setCardTitle(String cardTitle) { + this.cardTitle = cardTitle; + } + + public String getCardDescription() { + return cardDescription; + } + + public void setCardDescription(String cardDescription) { + this.cardDescription = cardDescription; + } + + public String getImgUrl() { + return imgUrl; + } + + public void setImgUrl(String imgUrl) { + this.imgUrl = imgUrl; + } + + public String getSongUrl() { + return songUrl; + } + + public void setSongUrl(String songUrl) { + this.songUrl = songUrl; + } + + public String getUrl() { + return url; + } + + public void setUrl(String url) { + this.url = url; + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/PaginationListener.java b/app/src/main/java/com/bernd32/jlyrics/PaginationListener.java new file mode 100644 index 0000000..2808a0f --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/PaginationListener.java @@ -0,0 +1,67 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +public abstract class PaginationListener extends RecyclerView.OnScrollListener { + + public static final int PAGE_START = 1; + @NonNull + private LinearLayoutManager layoutManager; + private static final int PAGE_SIZE = 20; + + public PaginationListener(@NonNull LinearLayoutManager layoutManager) { + this.layoutManager = layoutManager; + } + + @Override + public void onScrolled(@NonNull RecyclerView recyclerView, int dx, int dy) { + super.onScrolled(recyclerView, dx, dy); + + int visibleItemCount = layoutManager.getChildCount(); + int totalItemCount = layoutManager.getItemCount(); + int firstVisibleItemPosition = layoutManager.findFirstVisibleItemPosition(); + + if (!isLoading() && !isLastPage()) { + if ((visibleItemCount + firstVisibleItemPosition) >= totalItemCount + && firstVisibleItemPosition >= 0 + && totalItemCount >= PAGE_SIZE) { + loadMoreItems(); + } + } + + if (dy > 0 && firstVisibleItemPosition > 5) { + showFAB(); + } + + if(dy < 0 && firstVisibleItemPosition < 5) + hideFAB(); + } + + protected abstract void showFAB(); + + protected abstract void hideFAB(); + + protected abstract void loadMoreItems(); + + protected abstract boolean isLastPage(); + + protected abstract boolean isLoading(); +} \ No newline at end of file diff --git a/app/src/main/java/com/bernd32/jlyrics/adapters/FavLyricsAdapter.java b/app/src/main/java/com/bernd32/jlyrics/adapters/FavLyricsAdapter.java new file mode 100644 index 0000000..8af15d0 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/adapters/FavLyricsAdapter.java @@ -0,0 +1,133 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.adapters; + + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.jlyrics.BaseViewHolder; +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.database.FavLyrics; +import com.bernd32.jlyrics.ui.SearchActivity; +import com.bernd32.jlyrics.ui.ShowLyricsActivity; +import com.bumptech.glide.Glide; +import com.google.android.material.card.MaterialCardView; + +import java.util.List; + +public class FavLyricsAdapter extends RecyclerView.Adapter { + private static final String TAG = "PostRecyclerAdapter"; + private List mPostItems; + private Context mContext; + + public FavLyricsAdapter(Context context, List postItems) { + Log.d(TAG, "PostRecyclerAdapter: constructor activated"); + this.mPostItems = postItems; + this.mContext = context; + } + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { + return new ViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_listitem, parent, false)); + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { + holder.onBind(position); + } + + + @Override + public int getItemCount() { + return mPostItems == null ? 0 : mPostItems.size(); + } + + public void addItems(List postItems) { + mPostItems.addAll(postItems); + mPostItems = postItems; + notifyDataSetChanged(); + } + + public void clear() { + mPostItems.clear(); + notifyDataSetChanged(); + Glide.get(mContext).clearMemory(); + } + + public FavLyrics getItem(int position) { + return mPostItems.get(position); + } + + public class ViewHolder extends BaseViewHolder { + TextView cardTitle; + TextView cardDescription; + ImageView image; + MaterialCardView parentLayout; + + ViewHolder(View itemView) { + super(itemView); + cardTitle = itemView.findViewById(R.id.card_title); + cardDescription = itemView.findViewById(R.id.card_description); + image = itemView.findViewById(R.id.image); + parentLayout = itemView.findViewById(R.id.parent_layout); + } + + protected void clear() { + + } + + public void onBind(int position) { + super.onBind(position); + FavLyrics item = mPostItems.get(position); + cardTitle.setText(item.getTitle()); + cardDescription.setText(item.getDescription()); + // If no picture found, set default picture + if (item.getImgUrl().equals(SearchActivity.NO_IMG)) { + image.setVisibility(View.GONE); + } else { + image.setVisibility(View.VISIBLE); + Glide.with(mContext) + .asBitmap() + .load(item.getImgUrl()) + .into(image); + } + // click listener + parentLayout.setOnClickListener(view -> { + Intent intent = new Intent(mContext, ShowLyricsActivity.class); + intent.putExtra("song_url", item.getUrl()); + intent.putExtra("title", item.getTitle()); + intent.putExtra("description", item.getDescription()); + intent.putExtra("img_url", item.getImgUrl()); + mContext.startActivity(intent); + }); + } + } + +} + diff --git a/app/src/main/java/com/bernd32/jlyrics/adapters/PostRecyclerAdapter.java b/app/src/main/java/com/bernd32/jlyrics/adapters/PostRecyclerAdapter.java new file mode 100644 index 0000000..7bdd4d7 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/adapters/PostRecyclerAdapter.java @@ -0,0 +1,199 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.adapters; + +import android.content.Context; +import android.content.Intent; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ImageView; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.jlyrics.BaseViewHolder; +import com.bernd32.jlyrics.Lyric; +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.ui.ArtistSongsActivity; +import com.bernd32.jlyrics.ui.SearchActivity; +import com.bernd32.jlyrics.ui.ShowLyricsActivity; +import com.bumptech.glide.Glide; +import com.google.android.material.card.MaterialCardView; + +import java.util.List; + +public class PostRecyclerAdapter extends RecyclerView.Adapter { + private static final String TAG = "PostRecyclerAdapter"; + private static final int VIEW_TYPE_LOADING = 0; + private static final int VIEW_TYPE_NORMAL = 1; + public static final int SONG_PAGE = 0; + public static final int ARTIST_LYRICS_PAGE = 1; + public static final int ARTISTS_PAGE = 2; + public static final int LYRICS_PAGE = 3; + private boolean isLoaderVisible = false; + private List mPostItems; + private Context mContext; + private int pageType; + + public PostRecyclerAdapter(Context context, List postItems) { + Log.d(TAG, "PostRecyclerAdapter: constructor activated"); + this.mPostItems = postItems; + this.mContext = context; + } + + @NonNull + @Override + public BaseViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { + + switch (viewType) { + case VIEW_TYPE_NORMAL: + return new ViewHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.layout_listitem, parent, false)); + case VIEW_TYPE_LOADING: + return new ProgressHolder( + LayoutInflater.from(parent.getContext()).inflate(R.layout.item_loading, parent, false)); + default: + return null; + } + } + + @Override + public void onBindViewHolder(@NonNull BaseViewHolder holder, int position) { + holder.onBind(position); + } + + @Override + public int getItemViewType(int position) { + if (isLoaderVisible) { + return position == mPostItems.size() - 1 ? VIEW_TYPE_LOADING : VIEW_TYPE_NORMAL; + } else { + return VIEW_TYPE_NORMAL; + } + } + + @Override + public int getItemCount() { + return mPostItems == null ? 0 : mPostItems.size(); + } + + public void addItems(List postItems) { + mPostItems.addAll(postItems); + notifyDataSetChanged(); + } + + public void addLoading() { + isLoaderVisible = true; + mPostItems.add(new Lyric()); + notifyItemInserted(mPostItems.size() - 1); + } + + public void removeLoading() { + isLoaderVisible = false; + int position = mPostItems.size() - 1; + Lyric item = getItem(position); + if (item != null) { + mPostItems.remove(position); + notifyItemRemoved(position); + } + } + + public void clear() { + Log.d(TAG, "clear: started"); + // use it when performing new search + mPostItems.clear(); + notifyDataSetChanged(); + Glide.get(mContext).clearMemory(); + } + + private Lyric getItem(int position) { + return mPostItems.get(position); + } + + public void setPageType(int pageType) { + this.pageType = pageType; + } + + public class ViewHolder extends BaseViewHolder { + TextView cardTitle; + TextView cardDescription; + ImageView image; + MaterialCardView parentLayout; + + ViewHolder(View itemView) { + super(itemView); + cardTitle = itemView.findViewById(R.id.card_title); + cardDescription = itemView.findViewById(R.id.card_description); + image = itemView.findViewById(R.id.image); + parentLayout = itemView.findViewById(R.id.parent_layout); + } + + protected void clear() { + + } + + public void onBind(int position) { + super.onBind(position); + Lyric item = mPostItems.get(position); + cardTitle.setText(item.getCardTitle()); + cardDescription.setText(item.getCardDescription()); + Log.d(TAG, "onBind: item#"+position+" img url="+ item.getImgUrl()); + // If no picture found, set default picture + if (item.getImgUrl().equals(SearchActivity.NO_IMG)) { + image.setVisibility(View.GONE); + } else { + image.setVisibility(View.VISIBLE); + Glide.with(mContext) + .asBitmap() + .load(item.getImgUrl()) + .into(image); + } + // click listener + parentLayout.setOnClickListener(view -> { + Log.d(TAG, "onBind: " + pageType); + if (pageType == SONG_PAGE || pageType == ARTIST_LYRICS_PAGE) { + Intent intent = new Intent(mContext, ShowLyricsActivity.class); + intent.putExtra("song_url", item.getSongUrl()); + intent.putExtra("title", item.getCardTitle()); + intent.putExtra("description", item.getCardDescription()); + intent.putExtra("img_url", item.getImgUrl()); + mContext.startActivity(intent); + } else { + Intent intent = new Intent(mContext, ArtistSongsActivity.class); + intent.putExtra("url", item.getArtistUrl()); + intent.putExtra("title", item.getCardTitle()); + intent.putExtra("description", item.getCardDescription()); + intent.putExtra("img_url", item.getImgUrl()); + mContext.startActivity(intent); + } + + }); + } + } + + protected class ProgressHolder extends BaseViewHolder { + ProgressHolder(View itemView) { + super(itemView); + } + + @Override + protected void clear() { + } + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/async/AsyncTaskListener.java b/app/src/main/java/com/bernd32/jlyrics/async/AsyncTaskListener.java new file mode 100644 index 0000000..1fceb75 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/async/AsyncTaskListener.java @@ -0,0 +1,23 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.async; + +public interface AsyncTaskListener { + void onPreTask(); + void onPostTask(T object); + void onFailure(Exception e, int statusCode); +} \ No newline at end of file diff --git a/app/src/main/java/com/bernd32/jlyrics/async/GetDataAsyncTask.java b/app/src/main/java/com/bernd32/jlyrics/async/GetDataAsyncTask.java new file mode 100644 index 0000000..2f58c2e --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/async/GetDataAsyncTask.java @@ -0,0 +1,82 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.async; + +import android.os.AsyncTask; +import android.util.Log; + +import com.bernd32.jlyrics.utils.HelperClass; + +import org.jsoup.HttpStatusException; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +public class GetDataAsyncTask extends AsyncTask { // <[Input_Parameter Type], [Progress_Report Type], [Result Type]> + private static final String TAG = "GetDataAsyncTask"; + private Exception exception; + private int httpStatusCode; + private AsyncTaskListener asyncTaskListener; + + public GetDataAsyncTask(AsyncTaskListener asyncTaskListener) { + this.asyncTaskListener = asyncTaskListener; + } + + @Override + protected void onCancelled() { + super.onCancelled(); + } + + @Override + protected void onPreExecute() { + asyncTaskListener.onPreTask(); + super.onPreExecute(); + } + + @Override + protected Document doInBackground(String... params) { + String url = params[0]; + Log.d(TAG, "doInBackground: loading url: " + url); + Document doc; + String userAgent = HelperClass.getUserAgent(); + try { + doc = Jsoup + .connect(url) + .userAgent(userAgent) + .referrer("https://www.google.co.jp/") + .get(); + } catch (Exception e) { + if (e instanceof HttpStatusException) { + httpStatusCode = ((HttpStatusException) e).getStatusCode(); + } + exception = e; + Log.d(TAG, "doInBackground: " + e.getMessage()); + return null; + } + return doc.outputSettings(new Document.OutputSettings().prettyPrint(false)); + } + + protected void onPostExecute(Document s) { + super.onPostExecute(s); + if (asyncTaskListener != null) { + if (exception == null) { + asyncTaskListener.onPostTask(s); + } else { + asyncTaskListener.onFailure(exception, httpStatusCode); + } + } + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/async/PostDataAsyncTask.java b/app/src/main/java/com/bernd32/jlyrics/async/PostDataAsyncTask.java new file mode 100644 index 0000000..0ba5866 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/async/PostDataAsyncTask.java @@ -0,0 +1,96 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.async; + +import android.os.AsyncTask; +import android.util.Log; + +import com.bernd32.jlyrics.utils.HelperClass; + +import org.jsoup.Connection; +import org.jsoup.HttpStatusException; +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; + +/** + * Used for showing furigana + */ + +public class PostDataAsyncTask extends AsyncTask { // <[Input_Parameter Type], [Progress_Report Type], [Result Type]> + private static final String TAG = "PostDataAsyncTask"; + private Exception exception; + private int httpStatusCode; + private AsyncTaskListener listener; + + public PostDataAsyncTask(AsyncTaskListener listener) { + this.listener = listener; + } + + @Override + protected void onCancelled() { + super.onCancelled(); + } + + @Override + protected void onPreExecute() { + super.onPreExecute(); + listener.onPreTask(); + } + + @Override + protected Document doInBackground(String... params) { + // params[0] is a lyric text + String url; + url = "https://www.jcinfo.net/ja/tools/kana"; + Log.d(TAG, "doInBackground: params="+params[0]); + Document doc; + String userAgent = HelperClass.getUserAgent(); + try { + + Connection.Response response = Jsoup + .connect(url) + .method(Connection.Method.POST) + .userAgent(userAgent) + .referrer("https://www.google.co.jp/") + .data("txt", params[0]) + .execute(); + doc = response.parse(); + } catch (Exception e) { + if (e instanceof HttpStatusException) { + httpStatusCode = ((HttpStatusException) e).getStatusCode(); + } + exception = e; + Log.d(TAG, "doInBackground: " + e.getMessage()); + return null; + } + return doc; + } + + protected void onPostExecute(Document s) { + String htmlString = s.select("#main-content > div.dsp2.radius_5").html(); + Log.d(TAG, "onPostExecute: \n\n" + htmlString); + super.onPostExecute(s); + if (listener != null) { + if (exception == null) { + listener.onPostTask(htmlString); + } else { + listener.onFailure(exception, httpStatusCode); + } + } + + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/async/RomanizeAsyncTask.java b/app/src/main/java/com/bernd32/jlyrics/async/RomanizeAsyncTask.java new file mode 100644 index 0000000..c44fa3c --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/async/RomanizeAsyncTask.java @@ -0,0 +1,62 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.async; + + +import android.os.AsyncTask; + +import com.bernd32.romajihenkan.RomajiHenkan; + +/** + * Converting a Japanese text to romaji. + * Since this operation is time-consuming, it should be performed in a non-UI thread + */ + +public class RomanizeAsyncTask extends AsyncTask { + + private AsyncTaskListener listener; + + public RomanizeAsyncTask(AsyncTaskListener listener) { + this.listener = listener; + } + + + @Override + protected void onPreExecute() { + super.onPreExecute(); + listener.onPreTask(); + } + + @Override + protected void onPostExecute(String string) { + if (listener != null) { + listener.onPostTask(string); + } + super.onPostExecute(string); + } + + @Override + protected void onCancelled() { + super.onCancelled(); + } + + @Override + protected String doInBackground(String... strings) { + RomajiHenkan henkan = new RomajiHenkan(); + return henkan.convert(strings[0]); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/database/FavLyrics.java b/app/src/main/java/com/bernd32/jlyrics/database/FavLyrics.java new file mode 100644 index 0000000..d88ef6e --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/database/FavLyrics.java @@ -0,0 +1,88 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.database; + +import androidx.annotation.NonNull; +import androidx.room.ColumnInfo; +import androidx.room.Entity; +import androidx.room.PrimaryKey; + +@Entity(tableName = "lyrics") +public class FavLyrics { + + @PrimaryKey(autoGenerate = true) + private Integer id; + + @NonNull + @ColumnInfo(name = "title") + private String mTitle; + + @NonNull + @ColumnInfo(name = "description") + private String mDescription; + + @NonNull + @ColumnInfo(name = "url") + private String mUrl; + + @NonNull + @ColumnInfo(name = "img_url") + private String mImgUrl; + + @ColumnInfo(name = "time_stamp") + private long mTimeStamp; + + public FavLyrics(@NonNull String title, @NonNull String description, + @NonNull String url, @NonNull String imgUrl, long timeStamp) { + this.mTitle = title; + this.mDescription = description; + this.mUrl = url; + this.mImgUrl = imgUrl; + this.mTimeStamp = timeStamp; + } + + @NonNull + public String getTitle() { + return this.mTitle; + } + + @NonNull + public String getDescription() { + return this.mDescription; + } + + @NonNull + public String getUrl() { + return this.mUrl; + } + + @NonNull + public String getImgUrl() { + return this.mImgUrl; + } + + public long getTimeStamp() {return this.mTimeStamp; } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } +} + diff --git a/app/src/main/java/com/bernd32/jlyrics/database/LyricsDao.java b/app/src/main/java/com/bernd32/jlyrics/database/LyricsDao.java new file mode 100644 index 0000000..b9a7fb2 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/database/LyricsDao.java @@ -0,0 +1,50 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.database; + +import androidx.lifecycle.LiveData; +import androidx.room.Dao; +import androidx.room.Delete; +import androidx.room.Insert; +import androidx.room.OnConflictStrategy; +import androidx.room.Query; + +import java.util.List; + +@Dao +public interface LyricsDao { + + // Notifies its active observers when the data has changed. + + @Query("SELECT * from lyrics ORDER BY id DESC") + LiveData> getFavLyricsById(); + + @Insert(onConflict = OnConflictStrategy.IGNORE) + void insert(FavLyrics lyrics); + + @Query("DELETE FROM lyrics") + void deleteAll(); + + @Delete + void deleteLyrics(FavLyrics lyrics); + + @Query("SELECT count(*) FROM lyrics WHERE url = :url") + int isLyricsExists(String url); + + @Query("DELETE FROM lyrics WHERE url = :url") + void deleteLyricsByUrl(String url); +} diff --git a/app/src/main/java/com/bernd32/jlyrics/database/LyricsRepository.java b/app/src/main/java/com/bernd32/jlyrics/database/LyricsRepository.java new file mode 100644 index 0000000..7c1ef54 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/database/LyricsRepository.java @@ -0,0 +1,130 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.database; + +import android.app.Application; +import android.os.AsyncTask; + +import androidx.lifecycle.LiveData; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +class LyricsRepository { + + private LyricsDao mLyricsDao; + private LiveData> mAllFavLyrics; + + public LyricsRepository(Application application) { + LyricsRoomDatabase db = LyricsRoomDatabase.getDatabase(application); + mLyricsDao = db.lyricsDao(); + mAllFavLyrics = mLyricsDao.getFavLyricsById(); + } + + // Room executes all queries on a separate thread. + // Observed LiveData will notify the observer when the data has changed. + public LiveData> getAllLyrics() { + return mAllFavLyrics; + } + + // Must call this on a non-UI thread + public void insert(FavLyrics lyrics) { + LyricsRoomDatabase.databaseWriteExecutor.execute(() -> { + mLyricsDao.insert(lyrics); + }); + } + + public void deleteAll() { + new DeleteAllLyricsAsyncTask(mLyricsDao).execute(); + } + + // Need to run off main thread + public void deleteLyrics(FavLyrics lyrics) { + new DeleteLyricsAsyncTask(mLyricsDao).execute(lyrics); + } + + public boolean isLyricsExists(String url) throws ExecutionException, InterruptedException { + new SearchLyricsAsyncTask(mLyricsDao).execute(url); + SearchLyricsAsyncTask searchLyricsAsyncTask = new SearchLyricsAsyncTask(mLyricsDao); + return searchLyricsAsyncTask.execute(url).get(); + } + + + public void deleteLyricsByUrl(String url) { + new DeleteLyricsByURLAsyncTask(mLyricsDao).execute(url); + } + + + // Delete all favorites from the database (does not delete the table) + private static class DeleteAllLyricsAsyncTask extends AsyncTask { + private LyricsDao mAsyncTaskDao; + + DeleteAllLyricsAsyncTask(LyricsDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(Void... voids) { + mAsyncTaskDao.deleteAll(); + return null; + } + } + + // Delete a single favorite from the database. + + private static class DeleteLyricsAsyncTask extends AsyncTask { + private LyricsDao mAsyncTaskDao; + + DeleteLyricsAsyncTask(LyricsDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(final FavLyrics... params) { + mAsyncTaskDao.deleteLyrics(params[0]); + return null; + } + } + + private static class SearchLyricsAsyncTask extends AsyncTask { + private LyricsDao mAsyncTaskDao; + + SearchLyricsAsyncTask(LyricsDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Boolean doInBackground(final String... params) { + int count = mAsyncTaskDao.isLyricsExists(params[0]); + return count != 0; + } + } + + private static class DeleteLyricsByURLAsyncTask extends AsyncTask { + private LyricsDao mAsyncTaskDao; + + DeleteLyricsByURLAsyncTask(LyricsDao dao) { + mAsyncTaskDao = dao; + } + + @Override + protected Void doInBackground(final String... params) { + mAsyncTaskDao.deleteLyricsByUrl(params[0]); + return null; + } + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/database/LyricsRoomDatabase.java b/app/src/main/java/com/bernd32/jlyrics/database/LyricsRoomDatabase.java new file mode 100644 index 0000000..c1d5a75 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/database/LyricsRoomDatabase.java @@ -0,0 +1,69 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.database; + +import android.content.Context; + +import androidx.annotation.NonNull; +import androidx.room.Database; +import androidx.room.Room; +import androidx.room.RoomDatabase; +import androidx.sqlite.db.SupportSQLiteDatabase; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +@Database(entities = {FavLyrics.class}, version = 3, exportSchema = false) +public abstract class LyricsRoomDatabase extends RoomDatabase { + + abstract LyricsDao lyricsDao(); + + // marking the instance as volatile to ensure atomic access to the variable + private static volatile LyricsRoomDatabase INSTANCE; + private static final int NUMBER_OF_THREADS = 4; + static final ExecutorService databaseWriteExecutor = + Executors.newFixedThreadPool(NUMBER_OF_THREADS); + + static LyricsRoomDatabase getDatabase(final Context context) { + if (INSTANCE == null) { + synchronized (LyricsRoomDatabase.class) { + if (INSTANCE == null) { + INSTANCE = Room.databaseBuilder(context.getApplicationContext(), + LyricsRoomDatabase.class, "lyrics_database") + .fallbackToDestructiveMigration() + .addCallback(sRoomDatabaseCallback) + .build(); + } + } + } + return INSTANCE; + } + + /** + * Override the onOpen method to populate the database. + * For this sample, we clear the database every time it is created or opened. + * + * If you want to populate the database only when the database is created for the 1st time, + * override RoomDatabase.Callback()#onCreate + */ + private static RoomDatabase.Callback sRoomDatabaseCallback = new RoomDatabase.Callback() { + @Override + public void onOpen(@NonNull SupportSQLiteDatabase db) { + super.onOpen(db); + } + }; +} diff --git a/app/src/main/java/com/bernd32/jlyrics/database/LyricsViewModel.java b/app/src/main/java/com/bernd32/jlyrics/database/LyricsViewModel.java new file mode 100644 index 0000000..0fb5f4a --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/database/LyricsViewModel.java @@ -0,0 +1,70 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.database; + +import android.app.Application; + +import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; + +import java.util.List; +import java.util.concurrent.ExecutionException; + +/** + * View Model to keep a reference to the lyrics repository and + * an up-to-date list of all words. + */ + +public class LyricsViewModel extends AndroidViewModel { + + private LyricsRepository mRepository; + // Using LiveData and caching what getFavLyricsById returns has several benefits: + // - We can put an observer on the data (instead of polling for changes) and only update the + // the UI when the data actually changes. + // - Repository is completely separated from the UI through the ViewModel. + private LiveData> mAllLyrics; + + public LyricsViewModel(Application application) { + super(application); + mRepository = new LyricsRepository(application); + mAllLyrics = mRepository.getAllLyrics(); + } + + public LiveData> getAllLyrics() { + return mAllLyrics; + } + + public void insert(FavLyrics lyrics) { + mRepository.insert(lyrics); + } + + public void deleteAll() { + mRepository.deleteAll(); + } + + public void deleteLyrics(FavLyrics lyrics) { + mRepository.deleteLyrics(lyrics); + } + + public boolean isLyricsExists(String url) throws ExecutionException, InterruptedException { + return mRepository.isLyricsExists(url); + } + + public void deleteLyricsByUrl(String url) { + mRepository.deleteLyricsByUrl(url); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/search/CallbackInterface.java b/app/src/main/java/com/bernd32/jlyrics/search/CallbackInterface.java new file mode 100644 index 0000000..1f6a6e0 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/search/CallbackInterface.java @@ -0,0 +1,17 @@ +package com.bernd32.jlyrics.search; + +import com.bernd32.jlyrics.Lyric; + +import java.util.ArrayList; + +public interface CallbackInterface { + + void showAlertDialog(); + void setActivityTitle(int numberOfSongs); + void setPageType(int pageType); + void getSearchResults(ArrayList items); + void taskStarted(); + void taskFinished(); + void taskFailed(Exception e, int statusCode); + +} diff --git a/app/src/main/java/com/bernd32/jlyrics/search/SearchLyricsRepository.java b/app/src/main/java/com/bernd32/jlyrics/search/SearchLyricsRepository.java new file mode 100644 index 0000000..2e93e17 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/search/SearchLyricsRepository.java @@ -0,0 +1,230 @@ +package com.bernd32.jlyrics.search; + +import android.app.Application; +import android.net.Uri; +import android.util.Log; + +import com.bernd32.jlyrics.Lyric; +import com.bernd32.jlyrics.adapters.PostRecyclerAdapter; +import com.bernd32.jlyrics.async.AsyncTaskListener; +import com.bernd32.jlyrics.async.GetDataAsyncTask; + + +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.util.ArrayList; + +import static com.bernd32.jlyrics.adapters.PostRecyclerAdapter.ARTISTS_PAGE; +import static com.bernd32.jlyrics.adapters.PostRecyclerAdapter.SONG_PAGE; +import static com.bernd32.jlyrics.ui.SearchActivity.NO_IMG; + +class SearchLyricsRepository { + + private static final String TAG = "SearchLyricsRepository"; + private CallbackInterface listener; + private int pageType; + private int maxPage; + + SearchLyricsRepository(Application application) { + } + + void newSearchRequest(String url) { + getDocumentAsyncTask(url); + } + + private ArrayList artistSearch(Document doc) { + // Handles artist search result + final ArrayList items = new ArrayList<>(); + Elements imgElements = doc.select("#mnb > div.bdy"); + Elements artists = doc.select("#mnb > div > p.mid > a"); + Elements urls = doc.select("#mnb > div > p.mid > a"); + Elements lyricsNumber = doc.select("#mnb > div > p.sml:contains(収録数:)"); + int maxPages = 1; + // Show alert dialog if nothing found + if (artists.size() == 0) { + listener.showAlertDialog(); + } else { + for (int i = 0; i < artists.size(); i++) { + Lyric postItem = new Lyric(); + String getArtist = artists.get(i).text(); + String getNumberOfSongs = lyricsNumber.get(i).text() + .replace("収録数:", "Lyrics: ") + .replace("曲", ""); + String imgSrc = imgElements.get(i).select("a > img").attr("src"); + if (!imgSrc.isEmpty()) { + postItem.setImgUrl(imgSrc); + } else { + postItem.setImgUrl(NO_IMG); + } + String artistUrl = urls.get(i).attr("href"); + postItem.setCardTitle(getArtist); + postItem.setCardDescription(getNumberOfSongs); + postItem.setArtistUrl(artistUrl); + items.add(postItem); + } + listener.setActivityTitle(artists.size()); + setMaxPage(maxPage); + } + listener.getSearchResults(items); + return items; + } + + private ArrayList songSearch(Document doc) { + Log.d(TAG, "songSearch: started"); + final ArrayList items = new ArrayList<>(); + maxPage = 1; + Elements imgElements = doc.select("#mnb > div.bdy"); + Elements urls = doc.select("#mnb > div > p.mid > a"); + Elements songNames = doc.select("#mnb > div.bdy > p.mid"); + Elements artists = doc.select("#mnb > div.bdy > p.sml:contains(歌:)"); + Element pageUrlElement = doc.select("#pager > a").first(); + // Show alert dialog if nothing found + if (artists.size() == 0) { + listener.showAlertDialog(); + } else { + for (int i = 0; i < artists.size(); i++) { + Lyric lyric = new Lyric(); + String imgSrc = imgElements.get(i).select("a > img").attr("src"); + if (!imgSrc.isEmpty()) { + lyric.setImgUrl(imgSrc); + } else { + lyric.setImgUrl(NO_IMG); + } + String url = urls.get(i).attr("href"); + lyric.setSongUrl(url); + lyric.setCardTitle(songNames.get(i).text()); + lyric.setCardDescription(artists.get(i).text().replace("歌:", "")); + items.add(lyric); + maxPage = numberOfPages(getNumberOfSongsFound(pageUrlElement)); + setMaxPage(maxPage); + } + listener.setActivityTitle(getNumberOfSongsFound(pageUrlElement)); + } + listener.getSearchResults(items); + return items; + } + + private int getPageType(Document doc) { + String description = doc.select("meta[name=description]").first().attr("content"); + Elements lyricsNumber = doc.select("#mnb > div > p.sml:contains(収録数:)"); + // keywords for detecting a page type + final String search_result_keyword = "検索結果ページ"; + final String lyrics_page_keyword = "歌詞ページ"; + final String artist_list_keyword = "歌詞一覧ページ"; + final String artist_search_result_keyword = "歌手名に"; + if (description.contains(search_result_keyword) && lyricsNumber.isEmpty()) { + // Song search result (has pages) + Log.d(TAG, "getPageType: Search result page detected"); + pageType = PostRecyclerAdapter.SONG_PAGE; + //adapter.setPageType(pageType); + listener.setPageType(pageType); + return pageType; + + } else if (description.contains(lyrics_page_keyword)) { + Log.d(TAG, "getPageType: Lyrics page detected"); + pageType = PostRecyclerAdapter.LYRICS_PAGE; + //adapter.setPageType(pageType); + return pageType; + } else if (description.contains(artist_list_keyword)) { + Log.d(TAG, "getPageType: Artist list page detected"); + pageType = PostRecyclerAdapter.ARTIST_LYRICS_PAGE; + listener.setPageType(pageType); + return pageType; + } else if(description.contains(artist_search_result_keyword) && !lyricsNumber.isEmpty()) { + Log.d(TAG, "getPageType: Artist search result page detected"); + pageType = PostRecyclerAdapter.ARTISTS_PAGE; + listener.setPageType(pageType); + return pageType; + } else { + Log.d(TAG, "getPageType: cannot detect page type"); + return PostRecyclerAdapter.SONG_PAGE; + } + } + + private void getDocumentAsyncTask(String url) { + Log.d(TAG, "startAsyncTask: " + url); + GetDataAsyncTask task = new GetDataAsyncTask(new AsyncTaskListener() { + @Override + public void onPreTask() { + listener.taskStarted(); + } + + @Override + public void onPostTask(Document doc) { + pageType = getPageType(doc); + // Get lyrics from a song search type + if (pageType == SONG_PAGE) { + songSearch(doc); + } else if (pageType == ARTISTS_PAGE) { + artistSearch(doc); + } else { + artistSongList(doc); + } + listener.taskFinished(); + } + + @Override + public void onFailure(Exception e, int statusCode) { + listener.taskFailed(e, statusCode); + } + }); + task.execute(url); + } + + private ArrayList artistSongList(Document doc) { + // Handles artist's list of lyrics + final ArrayList items = new ArrayList<>(); + Elements imgElements = doc.select("#mnb > div.cnt > div[id^=ly]"); + Elements titles = doc.select("#mnb > div.cnt > div[id^=ly] > p.ttl > a"); + String description = doc.selectFirst("#mnb > div.cnt > div.cap > h2").text(); + for (int i = 0; i < titles.size(); i++) { + Lyric postItem = new Lyric(); + String imgSrc = imgElements.get(i).select("a > img.i5r").attr("src"); + if (!imgSrc.isEmpty()) { + Log.d(TAG, i + " = " + imgSrc); + postItem.setImgUrl(imgSrc); + } else { + Log.d(TAG, "artistDocumentHandle: no image found"); + postItem.setImgUrl(NO_IMG); + } + String title = titles.get(i).text(); + String url = titles.get(i).attr("href"); + postItem.setCardTitle(title); + postItem.setCardDescription(description.replace("の歌詞リスト", "")); // leave blank for now + postItem.setSongUrl("http://j-lyric.net" + url); + Log.d(TAG, "documentHandler: url = http://j-lyric.net" + url); + items.add(postItem); + } + listener.setActivityTitle(titles.size()); + listener.getSearchResults(items); + return items; + } + + private int getNumberOfSongsFound(Element pageUrlElement) { + String url = pageUrlElement.attr("href"); + Uri uri = Uri.parse(url); + String c = uri.getQueryParameter("c"); + assert c != null; + return Integer.valueOf(c); + } + + private int numberOfPages(int songs) { + double p = Math.ceil((double) songs/20); + return (int) p; + } + + public void addListener(CallbackInterface listener) { + this.listener = listener; + + } + + public void setMaxPage(int maxPage) { + this.maxPage = maxPage; + } + + public int getMaxPage() { + return maxPage; + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/search/SearchLyricsViewModel.java b/app/src/main/java/com/bernd32/jlyrics/search/SearchLyricsViewModel.java new file mode 100644 index 0000000..7f3251b --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/search/SearchLyricsViewModel.java @@ -0,0 +1,31 @@ +package com.bernd32.jlyrics.search; + +import android.app.Application; +import android.util.Log; + +import androidx.lifecycle.AndroidViewModel; + +public class SearchLyricsViewModel extends AndroidViewModel { + + private SearchLyricsRepository mRepository; + private static final String TAG = "SearchLyricsViewModel"; + + public SearchLyricsViewModel(Application application) { + super(application); + mRepository = new SearchLyricsRepository(application); + } + + public void newSearchRequest(String url) { + Log.d(TAG, "newSearchRequest: " + url); + mRepository.newSearchRequest(url); + } + + public void addListener(CallbackInterface callback) { + mRepository.addListener(callback); + } + + public int getMaxPage() { + Log.d(TAG, "getMaxPage: " + mRepository.getMaxPage()); + return mRepository.getMaxPage(); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/ArtistSongsActivity.java b/app/src/main/java/com/bernd32/jlyrics/ui/ArtistSongsActivity.java new file mode 100644 index 0000000..e46ff4f --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/ArtistSongsActivity.java @@ -0,0 +1,174 @@ +package com.bernd32.jlyrics.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.jlyrics.Lyric; +import com.bernd32.jlyrics.PaginationListener; +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.adapters.PostRecyclerAdapter; +import com.bernd32.jlyrics.async.GetDataAsyncTask; +import com.bernd32.jlyrics.search.CallbackInterface; +import com.bernd32.jlyrics.search.SearchLyricsViewModel; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.ArrayList; + +public class ArtistSongsActivity extends AppCompatActivity { + + private static final String TAG = "SearchActivity"; + private static final String NO_IMG = "no_img"; + @SuppressWarnings("WeakerAccess") + public RecyclerView mRecyclerView; + private ProgressBar progressBar; + private PostRecyclerAdapter adapter; + private String url; + private FloatingActionButton floatingActionButton; + private ExtendedFloatingActionButton cancelFAB; + private GetDataAsyncTask task; + + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate: started"); + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_search); + cancelFAB = findViewById(R.id.load_cancel); + progressBar = findViewById(R.id.progress_bar); + mRecyclerView = findViewById(R.id.recyclerv_view); + floatingActionButton = findViewById(R.id.floating_action_button); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setDisplayHomeAsUpEnabled(true); + Bundle extras = getIntent().getExtras(); + if (extras != null) { + loadIntents(); + } + mRecyclerView.setHasFixedSize(true); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + mRecyclerView.setLayoutManager(layoutManager); + adapter = new PostRecyclerAdapter(this, new ArrayList<>()); + mRecyclerView.setAdapter(adapter); + + SearchLyricsViewModel viewModel = new ViewModelProvider(this).get(SearchLyricsViewModel.class); + // show progressbar only in initial loading + CallbackInterface listener = new CallbackInterface() { + + @Override + public void showAlertDialog() { + + } + + @Override + public void setActivityTitle(int numberOfSongs) { + ArtistSongsActivity.this.setTitle(getString(R.string.title_found) + + " " + numberOfSongs); + } + + @Override + public void setPageType(int pageType) { + + } + + @Override + public void getSearchResults(ArrayList items) { + adapter.addItems(items); + } + + @Override + public void taskStarted() { + Log.d(TAG, "onPreTask: started"); + // show progressbar only in initial loading + progressBar.setVisibility(View.VISIBLE); + cancelFAB.setVisibility(View.VISIBLE); + } + + @Override + public void taskFinished() { + Log.d(TAG, "onPostTask: started"); + progressBar.setVisibility(View.GONE); + cancelFAB.setVisibility(View.GONE); + } + + @Override + public void taskFailed(Exception e, int statusCode) { + Toast.makeText(ArtistSongsActivity.this, + getString(R.string.connection_failed), + Toast.LENGTH_LONG).show(); + finish(); + Log.d(TAG, "onFailure: " + e.toString()); + } + }; + + viewModel.addListener(listener); + viewModel.newSearchRequest(url); + + floatingActionButton.setOnClickListener(view -> { + mRecyclerView.smoothScrollToPosition(0); + }); + mRecyclerView.addOnScrollListener(new PaginationListener(layoutManager) { + @Override + protected void loadMoreItems() { + floatingActionButton.setVisibility(View.VISIBLE); + } + + @Override + public boolean isLastPage() { + return true; + } + + @Override + public boolean isLoading() { + return false; + } + + @Override + protected void showFAB() { + if(floatingActionButton.getVisibility() != View.VISIBLE) { + floatingActionButton.show(); + } + } + + @Override + protected void hideFAB() { + if(floatingActionButton.getVisibility() == View.VISIBLE) { + floatingActionButton.hide(); + } + } + }); + } + + @Override + public boolean onSupportNavigateUp(){ + finish(); + return true; + } + + public void onLoadCancel(View view) { + if (task != null) + task.cancel(true); + finish(); + } + + private void loadIntents() { + Intent intent = getIntent(); + String title = intent.getStringExtra("title"); + String description = intent.getStringExtra("description"); + url = intent.getStringExtra("url"); + String imgUrl = intent.getStringExtra("img_url"); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/ChangeFontDialogFragment.java b/app/src/main/java/com/bernd32/jlyrics/ui/ChangeFontDialogFragment.java new file mode 100644 index 0000000..e3edced --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/ChangeFontDialogFragment.java @@ -0,0 +1,136 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.ui; + + +import android.annotation.SuppressLint; +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.SeekBar; +import android.widget.TextView; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.fragment.app.DialogFragment; + +import com.bernd32.jlyrics.R; + +import java.util.Objects; + +class ChangeFontDialogFragment extends DialogFragment { + private static final String TAG = "ChangeFontDialogFragmen"; + private OnDialogButtonClick listener; + + public void setOnDialogButtonClick(OnDialogButtonClick buttonClick) { + this.listener = buttonClick; + } + + @Override + public void onAttach(@NonNull Context context) { + super.onAttach(context); + // Instantiate the NoticeDialogListener so we can send events to the host + listener = (OnDialogButtonClick) context; + + } + + @Override + public void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate: started"); + super.onCreate(savedInstanceState); + } + + @Override + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { + Log.d(TAG, "onCreateView: started"); + return inflater.inflate(R.layout.dialog_change_fontsize, container, false); + } + + @Override + public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { + Log.d(TAG, "onViewCreated: started"); + super.onViewCreated(view, savedInstanceState); + + + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + Log.d(TAG, "onCreateDialog: started"); + // Use the Builder class for convenient dialog construction + AlertDialog.Builder builder = new AlertDialog.Builder(Objects.requireNonNull(getActivity())); + // Get the layout inflater + LayoutInflater inflater = requireActivity().getLayoutInflater(); + // Inflate and set the layout for the dialog + // Pass null as the parent view because its going in the dialog layout + @SuppressLint("InflateParams") View content = inflater.inflate(R.layout.dialog_change_fontsize, null); + builder.setView(content); + final TextView fontSizeTV = content.findViewById(R.id.font_size_textview); + final SeekBar seekBar = content.findViewById(R.id.seek_bar); + TextView lyricsTV = ((ShowLyricsActivity) getActivity()).lyricsTV; + // Convert received font size from px to sp metrics + float px = lyricsTV.getTextSize(); + float sp = px / getResources().getDisplayMetrics().scaledDensity; + seekBar.setProgress((int) sp); + fontSizeTV.setText(String.valueOf((int) sp)); + builder.setMessage(R.string.change_font) + .setPositiveButton(R.string.ok, (dialog, id) -> { + listener.onOkClicked(ChangeFontDialogFragment.this); + // Send data (seekBar.getProgress()) through the interface + // (OnDialogButtonClick.changeFontSize()) back to the activity (ShowLyricsActivty) + listener.changeFontSize(seekBar.getProgress()); + }) + .setNegativeButton(R.string.cancel, (dialog, id) -> { + // Send the negative button event back to the host activity + listener.onCancelClicked(ChangeFontDialogFragment.this); + }); + + seekBar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() { + + @Override + public void onStopTrackingTouch(SeekBar seekBar) { + + } + + @Override + public void onStartTrackingTouch(SeekBar seekBar) { + // TODO Auto-generated method stub + } + + @SuppressLint("SetTextI18n") + @Override + public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { + fontSizeTV.setText(" " + progress); + } + }); + // Create the AlertDialog object and return it + return builder.create(); + } + + public interface OnDialogButtonClick { + void onOkClicked(DialogFragment dialog); + void onCancelClicked(DialogFragment dialog); + void changeFontSize(int i); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/FavoritesActivity.java b/app/src/main/java/com/bernd32/jlyrics/ui/FavoritesActivity.java new file mode 100644 index 0000000..cfcac35 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/FavoritesActivity.java @@ -0,0 +1,123 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.ui; + +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.ItemTouchHelper; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.adapters.FavLyricsAdapter; +import com.bernd32.jlyrics.database.FavLyrics; +import com.bernd32.jlyrics.database.LyricsViewModel; + +import java.util.ArrayList; +import java.util.List; + +public class FavoritesActivity extends AppCompatActivity { + private static final String TAG = "FavoritesActivity"; + private LyricsViewModel lyricsViewModel; + @SuppressWarnings("WeakerAccess") + public RecyclerView recyclerView; // must not be private or static + private FavLyricsAdapter adapter; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate: has been called"); + + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_favorites); + recyclerView = findViewById(R.id.recycler_view_favs); + TextView emptyMsg = findViewById(R.id.empty_message); + // Find the toolbar view inside the activity layout + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setDisplayHomeAsUpEnabled(true); + recyclerView.setHasFixedSize(true); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + recyclerView.setLayoutManager(layoutManager); + adapter = new FavLyricsAdapter(this, new ArrayList<>()); + recyclerView.setAdapter(adapter); + lyricsViewModel = new ViewModelProvider(this).get(LyricsViewModel.class); + // Get all the lyrics from the database + // and associate them with the adapter + lyricsViewModel.getAllLyrics().observe(this, (List lyrics) -> { + // Update the cached copy of the lyrics in the adapter. + adapter.addItems(lyrics); + // Show a message if the list is empty + emptyMsg.setVisibility(adapter.getItemCount() == 0 ? View.VISIBLE : View.GONE); + }); + deleteFavoriteBySwipe(); + FavoritesActivity.this.setTitle(getString(R.string.favs)); + } + + private void deleteFavoriteBySwipe() { + ItemTouchHelper helper = new ItemTouchHelper( + new ItemTouchHelper.SimpleCallback(0, + ItemTouchHelper.LEFT | ItemTouchHelper.RIGHT) { + @Override + public boolean onMove(@NonNull RecyclerView recyclerView, + @NonNull RecyclerView.ViewHolder viewHolder, + @NonNull RecyclerView.ViewHolder target) { + return false; + } + @Override + // Delete an item from the database. + public void onSwiped(@NonNull RecyclerView.ViewHolder viewHolder, int direction) { + int position = viewHolder.getAdapterPosition(); + FavLyrics myLyrics = adapter.getItem(position); + Toast.makeText(FavoritesActivity.this, + getString(R.string.delete_lyrics_preamble), Toast.LENGTH_LONG).show(); + lyricsViewModel.deleteLyrics(myLyrics); + } + }); + // Attach the item touch helper to the recycler view + helper.attachToRecyclerView(recyclerView); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_favorites, menu); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + if (item.getItemId() == R.id.delete_all) { + lyricsViewModel.deleteAll(); + adapter.clear(); + Toast.makeText(this, "Cleared", Toast.LENGTH_SHORT).show(); + return true; + } + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/FuriganaActivity.java b/app/src/main/java/com/bernd32/jlyrics/ui/FuriganaActivity.java new file mode 100644 index 0000000..91418ae --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/FuriganaActivity.java @@ -0,0 +1,171 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.widget.ProgressBar; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.widget.Toolbar; + +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.async.AsyncTaskListener; +import com.bernd32.jlyrics.async.PostDataAsyncTask; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; + +public class FuriganaActivity extends AppCompatActivity { + + private static final String TAG = "FuriganaActivity"; + private ProgressBar progressBar; + private ExtendedFloatingActionButton cancelFAB; + private PostDataAsyncTask task; + private String savedHtmlString = ""; + private boolean darkTheme; + private float fontSize; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_furigana); + progressBar = findViewById(R.id.progress_bar); + cancelFAB = findViewById(R.id.load_cancel); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setDisplayHomeAsUpEnabled(true); + Intent intent = getIntent(); + String lyrics = intent.getStringExtra("lyrics_text"); + darkTheme = intent.getBooleanExtra("theme", false); + fontSize = intent.getFloatExtra("font_size", 20); + // Load lyrics from savedInstanceState, so we don't have to call startAsyncTask + // if activity is stopped or destroyed + if (savedInstanceState == null) { + startAsyncTask(lyrics); + } else { + savedHtmlString = savedInstanceState.getString("saved_lyrics"); + startWebView(savedHtmlString); + } + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + Log.d(TAG, "onSaveInstanceState: "); + if (!savedHtmlString.isEmpty()) { + outState.putString("saved_lyrics", savedHtmlString); + } + super.onSaveInstanceState(outState); + } + + private void startAsyncTask(String textToConvert) { + Log.d(TAG, "startAsyncTask: started"); + task = new PostDataAsyncTask(new AsyncTaskListener() { + + @Override + public void onPreTask() { + Log.d(TAG, "onPreTask: started"); + progressBar.setVisibility(View.VISIBLE); + cancelFAB.setVisibility(View.VISIBLE); + } + + @Override + public void onPostTask(String htmlString) { + Log.d(TAG, "onPostTask: started"); + // Save a string from AsyncTask to the local variable + savedHtmlString = htmlString; + progressBar.setVisibility(View.GONE); + cancelFAB.setVisibility(View.GONE); + startWebView(htmlString); + } + + @Override + public void onFailure(Exception e, int statusCode) { + Toast.makeText(FuriganaActivity.this, + getString(R.string.connection_failed), + Toast.LENGTH_LONG).show(); + finish(); + // TODO: 24.11.2019 log Exception e and int statusCode + Log.d(TAG, "onFailure: " + e.toString()); + } + }); + task.execute(textToConvert); + } + + public void onLoadCancel(View view) { + if (task != null) + task.cancel(true); + finish(); + } + + private void startWebView(String htmlString) { + WebView webView = findViewById(R.id.webview); + WebSettings webSettings = webView.getSettings(); + webSettings.setTextZoom(120); + webSettings.setSupportZoom(true); + webSettings.setBuiltInZoomControls(true); + webSettings.setDisplayZoomControls(false); + String html = setUpHtmlString(htmlString, (int)fontSize, darkTheme); + webSettings.setLayoutAlgorithm(WebSettings.LayoutAlgorithm.TEXT_AUTOSIZING); + webView.loadData(html,"text/html", "UTF-8"); + } + + private String setUpHtmlString(String htmlString, int fontSize, boolean darkTheme) { + String bgColor = darkTheme ? "black":"white"; + String fontColor = darkTheme ? "white":"black"; + String html = String.format("" + + "%s" + + "",fontColor, bgColor, htmlString); + Log.d(TAG, "setUpHtmlString: "+html); + return html; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + return super.onPrepareOptionsMenu(menu); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + Log.d(TAG, "onCreateOptionsMenu: started"); + return super.onCreateOptionsMenu(menu); + } + + @Override + public boolean onSupportNavigateUp(){ + if (task != null) + task.cancel(true); + finish(); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + return super.onOptionsItemSelected(item); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/LauncherActivity.java b/app/src/main/java/com/bernd32/jlyrics/ui/LauncherActivity.java new file mode 100644 index 0000000..5582143 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/LauncherActivity.java @@ -0,0 +1,214 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.ui; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.view.inputmethod.InputMethodManager; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.Spinner; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; +import androidx.constraintlayout.widget.Group; + +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.utils.PreferencesManager; + +public class LauncherActivity extends AppCompatActivity { + + private static final String TAG = "LauncherActivity"; + public static final String SEARCH_QUERY_SONG = "com.bernd32.jlyrics.SEARCH_QUERY_SONG"; + public static final String SPINNER_LIST = "com.bernd32.jlyrics.SPINNER_LIST"; + public static final String ARTIST_INPUT = "com.bernd32.jlyrics.ARTIST_INPUT"; + public static final String SONG_INPUT = "com.bernd32.jlyrics.SONG_INPUT"; + public static final String LYRICS_INPUT = "com.bernd32.jlyrics.LYRICS_INPUT"; + public static final String ARTIST_SPINNER = "com.bernd32.jlyrics.ARTIST_SPINNER"; + public static final String SONG_SPINNER = "com.bernd32.jlyrics.SONG_SPINNER"; + public static final String LYRICS_SPINNER = "com.bernd32.jlyrics.LYRICS_SPINNER"; + public static final String IGNORE_MAIN_SEARCH_INPUT = "com.bernd32.jlyrics.IGNORE_MAIN_SEARCH_INPUT"; + + private EditText mainSearchInput; + private EditText songInput; + private EditText artistInput; + private EditText lyricsInput; + private Spinner songSpinner; + private Spinner artistSpinner; + private Spinner lyricsSpinner; + private Spinner spinner; + private Button detailedSearchButton; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + mainSearchInput = findViewById(R.id.search_input); + spinner = findViewById(R.id.spinner); + songSpinner = findViewById(R.id.song_spinner); + artistSpinner = findViewById(R.id.artist_spinner); + lyricsSpinner = findViewById(R.id.lyrics_spinner); + songInput = findViewById(R.id.song_input); + artistInput = findViewById(R.id.artist_input); + lyricsInput = findViewById(R.id.lyrics_input); + detailedSearchButton = findViewById(R.id.detailed_search_button); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + // Create an ArrayAdapter using the string array and a default spinner layout + ArrayAdapter adapter = ArrayAdapter.createFromResource(this, + R.array.spinner_items, android.R.layout.simple_spinner_item); + ArrayAdapter adapter_d = ArrayAdapter.createFromResource(this, + R.array.spinner_search_options, android.R.layout.simple_spinner_item); + ArrayAdapter adapter_l = ArrayAdapter.createFromResource(this, + R.array.lyrics_spinner_options, android.R.layout.simple_spinner_item); + // Specify the layout to use when the list of choices appears + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + adapter_d.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + adapter_l.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + // Apply the adapter to the spinner + spinner.setAdapter(adapter); + songSpinner.setAdapter(adapter_d); + artistSpinner.setAdapter(adapter_d); + lyricsSpinner.setAdapter(adapter_l); + // Set default values to the spinners + songSpinner.setSelection(2); + artistSpinner.setSelection(2); + lyricsSpinner.setSelection(1); + // Load theme settings from PrefManager + AppCompatDelegate.setDefaultNightMode( + AppCompatDelegate.MODE_NIGHT_NO); + PreferencesManager.initializeInstance(this); + PreferencesManager pm = PreferencesManager.getInstance(); + if (pm.getDarkThemeSelected()) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_main, menu); + // Get saved data and set value to the checkable menu item + MenuItem darkThemeItem = menu.findItem(R.id.dark_theme); + PreferencesManager.initializeInstance(this); + PreferencesManager pm = PreferencesManager.getInstance(); + darkThemeItem.setChecked(pm.getDarkThemeSelected()); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.toolbar_show_favs: + Intent intent = new Intent(this, FavoritesActivity.class); + startActivity(intent); + return true; + case R.id.quit: + this.finishAffinity(); + return true; + case R.id.contact: + sendMail(getString(R.string.email_address), getString(R.string.email_subject_question)); + case R.id.dark_theme: + item.setChecked(!item.isChecked()); + // Save to value + PreferencesManager.initializeInstance(this); + PreferencesManager pm = PreferencesManager.getInstance(); + pm.setDarkThemeSelected(item.isChecked()); + if (item.isChecked()) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void sendMail(String email, String subject) { + String[] email_address = new String[] {email}; + Intent mailto = new Intent(Intent.ACTION_SENDTO, Uri.fromParts("mailto", email, null)); + mailto.putExtra(Intent.EXTRA_EMAIL, email_address); + mailto.putExtra(Intent.EXTRA_SUBJECT, subject); + startActivity(Intent.createChooser(mailto, "Send E-mail")); + } + + public void onLoadLyrics(View view) { + Intent intent = new Intent(this, SearchActivity.class); + boolean ignoreMainSearchInput = true; + String mainSearchText = mainSearchInput.getText().toString().trim(); + int spinnerPos = spinner.getSelectedItemPosition(); + String artistText = artistInput.getText().toString().trim(); + String songText = songInput.getText().toString().trim(); + String lyricsText = lyricsInput.getText().toString().trim(); + int artistSpinnerPos = artistSpinner.getSelectedItemPosition(); + int songSpinnerPos = songSpinner.getSelectedItemPosition(); + int lyricsSpinnerPos = lyricsSpinner.getSelectedItemPosition(); + if (artistText.isEmpty() && songText.isEmpty() && lyricsText.isEmpty()) { + ignoreMainSearchInput = false; + } + if (!mainSearchText.isEmpty() && !ignoreMainSearchInput) { + Log.d(TAG, "onLoadLyrics: user didn't use detailed search"); + intent.putExtra(SEARCH_QUERY_SONG, mainSearchText); + intent.putExtra(SPINNER_LIST, spinnerPos); + } + if (ignoreMainSearchInput){ + Log.d(TAG, "onLoadLyrics: user used detailed search"); + intent.putExtra(ARTIST_INPUT, artistText); + intent.putExtra(SONG_INPUT, songText); + intent.putExtra(LYRICS_INPUT, lyricsText); + intent.putExtra(ARTIST_SPINNER, artistSpinnerPos); + intent.putExtra(SONG_SPINNER, songSpinnerPos); + intent.putExtra(LYRICS_SPINNER, lyricsSpinnerPos); + } + if (intent.resolveActivity(getPackageManager()) != null && + (!mainSearchText.isEmpty() || ignoreMainSearchInput)) { + intent.putExtra(IGNORE_MAIN_SEARCH_INPUT, ignoreMainSearchInput); + startActivity(intent); + } + } + + public void onDetailedSearch(View view) { + Group group = findViewById(R.id.group); + if (group.getVisibility() == View.GONE) { + // Hide keyboard + view = this.getCurrentFocus(); + if (view != null) { + InputMethodManager imm = (InputMethodManager) getSystemService(Context.INPUT_METHOD_SERVICE); + assert imm != null; + imm.hideSoftInputFromWindow(view.getWindowToken(), 0); + } + group.setVisibility(View.VISIBLE); + detailedSearchButton.setText(getString(R.string.detailed_search_hide)); + } + else { + group.setVisibility(View.GONE); + detailedSearchButton.setText(getString(R.string.detailed_search_show)); + } + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/SearchActivity.java b/app/src/main/java/com/bernd32/jlyrics/ui/SearchActivity.java new file mode 100644 index 0000000..679271c --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/SearchActivity.java @@ -0,0 +1,322 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.ui; + +import android.content.Intent; +import android.os.Bundle; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.Toolbar; +import androidx.lifecycle.ViewModelProvider; +import androidx.recyclerview.widget.LinearLayoutManager; +import androidx.recyclerview.widget.RecyclerView; + +import com.bernd32.jlyrics.PaginationListener; +import com.bernd32.jlyrics.Lyric; +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.adapters.PostRecyclerAdapter; +import com.bernd32.jlyrics.async.GetDataAsyncTask; +import com.bernd32.jlyrics.search.CallbackInterface; +import com.bernd32.jlyrics.search.SearchLyricsViewModel; +import com.bernd32.jlyrics.utils.PreferencesManager; +import com.bernd32.romajihenkan.RomajiHenkan; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; +import com.google.android.material.floatingactionbutton.FloatingActionButton; + +import java.util.ArrayList; + +import static com.bernd32.jlyrics.PaginationListener.PAGE_START; +import static com.bernd32.jlyrics.utils.HelperClass.urlBuilder; + +public class SearchActivity extends AppCompatActivity { + + private static final String TAG = "SearchActivity"; + public static final String NO_IMG = "no_img"; + @SuppressWarnings("WeakerAccess") + public RecyclerView mRecyclerView; // must not be private or static + private ProgressBar progressBar; + private PostRecyclerAdapter adapter; + private int currentPage = PAGE_START; + private boolean isLastPage = false; + private boolean isLoading = false; + private String searchInput; + private int spinnerPos; + private String artistText; + private String songText; + private String lyricsText; + private int artistSpinnerPos; + private int songSpinnerPos; + private int lyricsSpinnerPos; + private boolean ignoreMainSearch; + private FloatingActionButton floatingActionButton; + private ExtendedFloatingActionButton cancelFAB; + private GetDataAsyncTask task; + private SearchLyricsViewModel viewModel; + private int maxPage; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // Get intents + Bundle extras = getIntent().getExtras(); + if (extras != null) { + intentLoader(); + } + // UI + setContentView(R.layout.activity_search); + progressBar = findViewById(R.id.progress_bar); + mRecyclerView = findViewById(R.id.recyclerv_view); + floatingActionButton = findViewById(R.id.floating_action_button); + cancelFAB = findViewById(R.id.load_cancel); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setDisplayHomeAsUpEnabled(true); + mRecyclerView.setHasFixedSize(true); + LinearLayoutManager layoutManager = new LinearLayoutManager(this); + mRecyclerView.setLayoutManager(layoutManager); + adapter = new PostRecyclerAdapter(this, new ArrayList<>()); + mRecyclerView.setAdapter(adapter); + // initial load + String url = ignoreMainSearch ? + urlBuilder(PAGE_START, artistText, artistSpinnerPos, songText, + songSpinnerPos, lyricsText, lyricsSpinnerPos) : + urlBuilder(PAGE_START, spinnerPos, searchInput); + viewModel = new ViewModelProvider(this).get(SearchLyricsViewModel.class); + CallbackInterface listener = new CallbackInterface() { + + // Show alert dialog in case when no search results were found + @Override + public void showAlertDialog() { + AlertDialog.Builder builder = new AlertDialog.Builder(SearchActivity.this); + builder.setTitle(getString(R.string.not_found_title)); + builder.setMessage(getString(R.string.not_found_message)); + builder.setPositiveButton(R.string.ok, (dialog, id) -> { + dialog.dismiss(); + finish(); + }); + AlertDialog dialog = builder.create(); + dialog.show(); + } + + // Show number of search result found in the action bar + @Override + public void setActivityTitle(int numberOfSongs) { + SearchActivity.this.setTitle(getString(R.string.title_found) + + " " + numberOfSongs); + } + + // Tell our adapter the search result type we got, so adapter's click listener + // will know what activity show be opened when we click on a search result + @Override + public void setPageType(int pageType) { + adapter.setPageType(pageType); + } + + // This is basically the main callback method that takes the search + // result items from the view model and loads them into the UI via our recycler adapter + // This callback method also handles the pagination future, and loads new search + // result pages into our adapter + @Override + public void getSearchResults(ArrayList items) { + maxPage = viewModel.getMaxPage(); + newPageLoader(maxPage, items); + /* + * add scroll listener while user reach in bottom load more will call + */ + mRecyclerView.addOnScrollListener(new PaginationListener(layoutManager) { + @Override + protected void loadMoreItems() { + isLoading = true; + currentPage++; + String url = ignoreMainSearch ? + urlBuilder(currentPage, artistText, artistSpinnerPos, songText, + songSpinnerPos, lyricsText, lyricsSpinnerPos) : + urlBuilder(currentPage, spinnerPos, searchInput); + floatingActionButton.setVisibility(View.VISIBLE); + // Load search results from a new page + viewModel.newSearchRequest(url); + } + + @Override + public boolean isLastPage() { + return isLastPage; + } + + @Override + public boolean isLoading() { + return isLoading; + } + + @Override + protected void showFAB() { + if (floatingActionButton.getVisibility() != View.VISIBLE) { + floatingActionButton.show(); + } + } + + @Override + protected void hideFAB() { + if (floatingActionButton.getVisibility() == View.VISIBLE) { + floatingActionButton.hide(); + } + } + }); + } + + // Showing loading animation when user started searching + @Override + public void taskStarted() { + if (currentPage == 1) { + progressBar.setVisibility(View.VISIBLE); + mRecyclerView.setVisibility(View.GONE); + cancelFAB.setVisibility(View.VISIBLE); + } + } + + // Hide loading animation and show search results + @Override + public void taskFinished() { + Log.d(TAG, "onPostTask: started"); + if (currentPage == 1) { + progressBar.setVisibility(View.GONE); + mRecyclerView.setVisibility(View.VISIBLE); + cancelFAB.setVisibility(View.GONE); + } + } + + // Shows a toast when we got an exception and returns back to main activity + @Override + public void taskFailed(Exception e, int statusCode) { + Toast.makeText(SearchActivity.this, + getString(R.string.connection_failed), + Toast.LENGTH_LONG).show(); + finish(); + // TODO: 24.11.2019 log Exception e and int statusCode + Log.e(TAG, "onFailure: " + e.toString()); + } + }; + + floatingActionButton.setOnClickListener(view -> { + mRecyclerView.smoothScrollToPosition(0); + }); + + viewModel.addListener(listener); + // Send a request to parse a search results from an URL and get results back + // to activity via callback method getSearchResults() + viewModel.newSearchRequest(url); + } + + private void newPageLoader(int maxPage, ArrayList items) { + if (currentPage != PAGE_START) { + mRecyclerView.post(() -> adapter.removeLoading()); + } + mRecyclerView.post(() -> adapter.addItems(items)); + // check whether is last page or not + if (currentPage < maxPage) { + mRecyclerView.post(() -> adapter.addLoading()); + } else { + isLastPage = true; + } + isLoading = false; + } + + @Override + public boolean onSupportNavigateUp(){ + finish(); + return true; + } + + @Override + protected void onDestroy() { + super.onDestroy(); + Log.d(TAG, "onDestroy: activated"); + adapter.clear(); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + getMenuInflater().inflate(R.menu.menu_main, menu); + // Get saved data and set value to the checkable menu item + MenuItem darkThemeItem = menu.findItem(R.id.dark_theme); + PreferencesManager.initializeInstance(this); + PreferencesManager pm = PreferencesManager.getInstance(); + darkThemeItem.setChecked(pm.getDarkThemeSelected()); + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + switch (item.getItemId()) { + case R.id.toolbar_show_favs: + // Open favorites activity + Intent intent = new Intent(this, FavoritesActivity.class); + startActivity(intent); + return true; + case R.id.quit: + this.finishAffinity(); + return true; + case R.id.dark_theme: + // Switch color theme of the app (light/dark) + item.setChecked(!item.isChecked()); + // Save to value + PreferencesManager.initializeInstance(this); + PreferencesManager pm = PreferencesManager.getInstance(); + pm.setDarkThemeSelected(item.isChecked()); + if (item.isChecked()) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void intentLoader() { + Intent intent = getIntent(); + searchInput = intent.getStringExtra(LauncherActivity.SEARCH_QUERY_SONG); + spinnerPos = intent.getIntExtra(LauncherActivity.SPINNER_LIST, 0); + artistText = intent.getStringExtra(LauncherActivity.ARTIST_INPUT); + songText = intent.getStringExtra(LauncherActivity.SONG_INPUT); + lyricsText = intent.getStringExtra(LauncherActivity.LYRICS_INPUT); + artistSpinnerPos = intent.getIntExtra(LauncherActivity.ARTIST_SPINNER, 0); + songSpinnerPos = intent.getIntExtra(LauncherActivity.SONG_SPINNER, 0); + lyricsSpinnerPos = intent.getIntExtra(LauncherActivity.LYRICS_SPINNER, 0); + ignoreMainSearch = intent.getBooleanExtra(LauncherActivity.IGNORE_MAIN_SEARCH_INPUT, false); + } + + public void onLoadCancel(View view) { + if (task != null) { + task.cancel(true); + task = null; + } + finish(); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/ui/ShowLyricsActivity.java b/app/src/main/java/com/bernd32/jlyrics/ui/ShowLyricsActivity.java new file mode 100644 index 0000000..c17eaec --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/ui/ShowLyricsActivity.java @@ -0,0 +1,383 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.ui; + +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.Intent; +import android.graphics.Typeface; +import android.os.Bundle; +import android.text.Spannable; +import android.text.SpannableString; +import android.text.style.StyleSpan; +import android.util.Log; +import android.view.Menu; +import android.view.MenuItem; +import android.view.View; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.ActionBar; +import androidx.appcompat.app.AppCompatActivity; +import androidx.appcompat.app.AppCompatDelegate; +import androidx.appcompat.widget.ShareActionProvider; +import androidx.appcompat.widget.Toolbar; +import androidx.core.view.MenuItemCompat; +import androidx.fragment.app.DialogFragment; +import androidx.lifecycle.ViewModelProvider; + +import com.bernd32.jlyrics.R; +import com.bernd32.jlyrics.async.AsyncTaskListener; +import com.bernd32.jlyrics.async.GetDataAsyncTask; +import com.bernd32.jlyrics.async.RomanizeAsyncTask; +import com.bernd32.jlyrics.database.FavLyrics; +import com.bernd32.jlyrics.database.LyricsViewModel; +import com.bernd32.jlyrics.utils.PreferencesManager; +import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton; + +import org.jsoup.nodes.Document; + +import java.util.Locale; +import java.util.concurrent.ExecutionException; + +import static com.bernd32.jlyrics.utils.HelperClass.addNewLines; + +public class ShowLyricsActivity extends AppCompatActivity implements ChangeFontDialogFragment.OnDialogButtonClick { + + private static final String TAG = "ShowLyricsActivity"; + private ProgressBar progressBar; + public TextView lyricsTV; + private String lyrics = ""; + private String romanizedLyrics = ""; + private String title; + private String songUrl; + private String cardDesc; + private String cardTitle; + private String imgUrl; + private LyricsViewModel lyricsViewModel; + private boolean hasLyrics; + private ExtendedFloatingActionButton cancelFAB; + private GetDataAsyncTask task; + private boolean romanized = false; + + @Override + protected void onCreate(Bundle savedInstanceState) { + Log.d(TAG, "onCreate: activated"); + super.onCreate(savedInstanceState); + PreferencesManager.initializeInstance(this); + setContentView(R.layout.activity_show_lyrics); + lyricsTV = findViewById(R.id.lyrics); + progressBar = findViewById(R.id.progress_bar); + cancelFAB = findViewById(R.id.load_cancel); + Toolbar toolbar = findViewById(R.id.toolbar); + setSupportActionBar(toolbar); + ActionBar actionBar = getSupportActionBar(); + assert actionBar != null; + actionBar.setDisplayHomeAsUpEnabled(true); + // For saving lyrics to favs + lyricsViewModel = new ViewModelProvider(this).get(LyricsViewModel.class); + loadIntents(); + try { + isLyricsInDB(songUrl); + } catch (ExecutionException | InterruptedException e) { + e.printStackTrace(); + } + // Load lyrics from savedInstanceState, so we don't have to call startAsyncTask + // if activity is stopped or destroyed + loadLyrics(savedInstanceState); + } + + private void loadLyrics(Bundle savedInstanceState) { + if (savedInstanceState == null) { + startAsyncTask(songUrl); + } else { + romanized = savedInstanceState.getBoolean("romanized"); + lyrics = savedInstanceState.getString("saved_lyrics"); + romanizedLyrics = savedInstanceState.getString("saved_romaji_lyrics"); + if (!romanized) { + lyricsTV.setText(lyrics); + } else { + lyricsTV.setText(romanizedLyrics); + } + } + } + + @Override + protected void onStop() { + Log.d(TAG, "onStop: started"); + super.onStop(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + Log.d(TAG, "onSaveInstanceState: start"); + outState.putString("saved_lyrics", lyrics); + outState.putString("saved_romaji_lyrics", romanizedLyrics); + outState.putBoolean("romanized", romanized); + + super.onSaveInstanceState(outState); + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + // Inflate the menu; this adds items to the action bar if it is present. + getMenuInflater().inflate(R.menu.menu_lyrics, menu); + MenuItem menuShareItem = menu.findItem(R.id.toolbar_share); + ShareActionProvider shareActionProvider = (ShareActionProvider) + MenuItemCompat.getActionProvider(menuShareItem); + // Get saved data and set values + MenuItem darkThemeItem = menu.findItem(R.id.dark_theme); + PreferencesManager pm = PreferencesManager.getInstance(); + darkThemeItem.setChecked(pm.getDarkThemeSelected()); + int fontSize = pm.getFontSize(); + lyricsTV.setTextSize((float) fontSize); + return true; + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + switch (item.getItemId()) { + case R.id.romanize: + convertLyrics(); + return true; + case R.id.toolbar_share: + Intent sendIntent = new Intent(); + sendIntent.setAction(Intent.ACTION_SEND); + sendIntent.putExtra(Intent.EXTRA_TEXT, lyrics); + sendIntent.setType("text/plain"); + Intent shareIntent = Intent.createChooser(sendIntent, title); + startActivity(shareIntent); + return true; + case R.id.toolbar_furigana: + openFuriganaActivity(); + return true; + case R.id.fav_add_remove: + saveLyrics(); + return true; + case R.id.dark_theme: + item.setChecked(!item.isChecked()); + PreferencesManager pm = PreferencesManager.getInstance(); + pm.setDarkThemeSelected(item.isChecked()); + if (item.isChecked()) { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES); + } else { + AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO); + } + return true; + case R.id.font_size: + showFontSizeDialog(); + return true; + case R.id.quit: + this.finishAffinity(); + return true; + case R.id.copy: + ClipboardManager clipboard = (ClipboardManager) getSystemService(Context.CLIPBOARD_SERVICE); + ClipData clip = ClipData.newPlainText(getString(R.string.lyrics), lyrics); + assert clipboard != null; + clipboard.setPrimaryClip(clip); + Toast.makeText(this, getString(R.string.copied), Toast.LENGTH_SHORT).show(); + return true; + default: + return super.onOptionsItemSelected(item); + } + } + + private void convertLyrics() { + if (!romanized) { + RomanizeAsyncTask romanizeTask = new RomanizeAsyncTask(new AsyncTaskListener() { + @Override + public void onPreTask() { + lyricsTV.setText(""); + progressBar.setVisibility(View.VISIBLE); + cancelFAB.setVisibility(View.VISIBLE); + } + + @Override + public void onPostTask(String string) { + progressBar.setVisibility(View.GONE); + cancelFAB.setVisibility(View.GONE); + ShowLyricsActivity.this.romanizedLyrics = string; + lyricsTV.setText(string); + romanized = true; + } + + @Override + public void onFailure(Exception e, int statusCode) { + Toast.makeText(ShowLyricsActivity.this, + getString(R.string.connection_failed), + Toast.LENGTH_LONG).show(); + finish(); + // TODO: 24.11.2019 log Exception e and int statusCode + Log.d(TAG, "onFailure: " + e.toString()); + } + }); + romanizeTask.execute(lyrics); + } else { + // If the text is already converted to romaji then show original Japanese text + lyricsTV.setText(lyrics); + romanized = false; + } + } + + private void openFuriganaActivity() { + PreferencesManager pm = PreferencesManager.getInstance(); + // Create an intent and open a new activity + Intent intent = new Intent(this, FuriganaActivity.class); + intent.putExtra("lyrics_text", lyrics); + intent.putExtra("font_size", lyricsTV.getTextSize()); + intent.putExtra("theme", pm.getDarkThemeSelected()); + if (intent.resolveActivity(getPackageManager()) != null) { + startActivity(intent); + } + } + + @Override + public void onOkClicked(DialogFragment dialog) { + Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show(); + } + + @Override + public void onCancelClicked(DialogFragment dialog) { + } + + @Override + public void changeFontSize(int i) { + // Get data from DialogFragment interface, + // change font size of lyric text and save the value + lyricsTV.setTextSize((float) i); + PreferencesManager pm = PreferencesManager.getInstance(); + pm.setFontSize(i); + } + + @Override + public boolean onSupportNavigateUp(){ + if (task != null) + task.cancel(true); + finish(); + return true; + } + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + if (hasLyrics) + menu.findItem(R.id.fav_add_remove).setTitle(getString(R.string.favs_remove)); + else + menu.findItem(R.id.fav_add_remove).setTitle(getString(R.string.favs_add)); + + if (romanized) { + menu.findItem(R.id.romanize).setTitle(getString(R.string.show_original)); + } else { + menu.findItem(R.id.romanize).setTitle(getString(R.string.romanize)); + } + return super.onPrepareOptionsMenu(menu); + } + + public void onLoadCancel(View view) { + if (task != null) + task.cancel(true); + finish(); + } + + private void showFontSizeDialog() { + DialogFragment changeFontAlert = new ChangeFontDialogFragment(); + changeFontAlert.show(getSupportFragmentManager(), "changeFontAlert"); + } + + private void loadIntents() { + if (getIntent().hasExtra("song_url")){ + Log.d(TAG, "getIncomingIntent: found intent extras."); + songUrl = getIntent().getStringExtra("song_url"); + } + imgUrl = getIntent().getStringExtra("img_url"); + cardTitle = getIntent().getStringExtra("title"); + cardDesc = getIntent().getStringExtra("description"); + } + + private void isLyricsInDB(String songUrl) throws ExecutionException, InterruptedException { + this.hasLyrics = lyricsViewModel.isLyricsExists(songUrl); + } + + private void saveLyrics() { + if (!hasLyrics) { + long timestamp = System.currentTimeMillis(); + FavLyrics savedLyrics = new FavLyrics(cardTitle, cardDesc, songUrl, imgUrl, timestamp); + lyricsViewModel.insert(savedLyrics); + Toast.makeText(this, getString(R.string.saved), Toast.LENGTH_SHORT).show(); + this.hasLyrics = true; + } else { + // Remove lyrics + lyricsViewModel.deleteLyricsByUrl(songUrl); + Toast.makeText(this, getString(R.string.removed), Toast.LENGTH_SHORT).show(); + this.hasLyrics = false; + finish(); + } + } + + private void startAsyncTask(String url) { + Log.d(TAG, "startAsyncTask: " + url); + task = new GetDataAsyncTask(new AsyncTaskListener() { + @Override + public void onPreTask() { + Log.d(TAG, "onPreTask: started"); + // show progressbar only in initial loading + progressBar.setVisibility(View.VISIBLE); + cancelFAB.setVisibility(View.VISIBLE); + } + + @Override + public void onPostTask(Document doc) { + Log.d(TAG, "onPostTask: started"); + progressBar.setVisibility(View.GONE); + cancelFAB.setVisibility(View.GONE); + showLyrics(doc); + } + + @Override + public void onFailure(Exception e, int statusCode) { + Toast.makeText(ShowLyricsActivity.this, + getString(R.string.connection_failed), + Toast.LENGTH_LONG).show(); + finish(); + // TODO: 24.11.2019 log Exception e and int statusCode + } + }); + task.execute(url); + } + + private void showLyrics(Document doc) { + Log.d(TAG, "showLyrics: activated"); + lyrics = doc.select("#Lyric").first().html(); + String songName = doc.select("#mnb > div.cap > h2") + .text() + .replace(" 歌詞", ""); + title = String.format(Locale.getDefault(), "「%s」\n\n\n", songName); + ShowLyricsActivity.this.setTitle(title); + lyrics = addNewLines(lyrics); + lyrics = lyrics.replace("(△くり返し)", getText(R.string.repeat)); + lyrics = lyrics.replace("(※くり返し)", getText(R.string.repeat2)); + SpannableString songTitle = new SpannableString(title); + songTitle.setSpan(new StyleSpan(Typeface.BOLD), 0, title.length(), Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + lyricsTV.setText(songTitle); + lyricsTV.append(lyrics); + Log.d(TAG, "Current artist: " + cardDesc); + Log.d(TAG, "Current song: " + cardTitle); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/utils/HelperClass.java b/app/src/main/java/com/bernd32/jlyrics/utils/HelperClass.java new file mode 100644 index 0000000..47f7b57 --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/utils/HelperClass.java @@ -0,0 +1,87 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.utils; + +import org.jsoup.Jsoup; +import org.jsoup.nodes.Document; +import org.jsoup.safety.Whitelist; + +import java.util.Arrays; +import java.util.List; +import java.util.Locale; +import java.util.concurrent.ThreadLocalRandom; + +public class HelperClass { + static final List userAgents = Arrays.asList("Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_1_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/16D57", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_2 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.1 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/11.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.1.2 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 13_1_3 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/13.0.1 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 12_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1", + "Mozilla/5.0 (iPhone; CPU iPhone OS 11_4_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15G77", + "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.169 Safari/537.36", + "Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/72.0.3626.121 Safari/537.36"); + + public static String getUserAgent() { + return userAgents.get(ThreadLocalRandom.current().nextInt((userAgents.size()))); + } + + public static String urlBuilder(int page, int spinnerPos, String searchInput) { + // Used for simple search + StringBuilder url = new StringBuilder("http://search.j-lyric.net/index.php?&ct=2&ca=2&cl=2&p="); + final int SONG_NAME_SELECTED = 0; + final int ARTIST_SELECTED = 1; + final int LYRICS_SELECTED = 2; + switch (spinnerPos) { + case SONG_NAME_SELECTED: + url.append(page).append("&kt=").append(searchInput); + break; + case ARTIST_SELECTED: + url.append(page).append("&ka=").append(searchInput); + break; + case LYRICS_SELECTED: + url.append(page).append("&kl=").append(searchInput); + break; + } + return url.toString(); + } + + public static String urlBuilder(int page, String artistText, int artistSpinnerPos, String songText, + int songSpinnerPos, String lyricsText, int lyricsSpinnerPos) { + // Used for detailed search + if (lyricsSpinnerPos > 0) lyricsSpinnerPos += 1; + String url = String.format(Locale.getDefault(), + "http://search.j-lyric.net/index.php?p=%d&kt=%s&ct=%d&ka=%s&ca=%d&kl=%s&cl=%d", + page, songText, songSpinnerPos, artistText, artistSpinnerPos, lyricsText, lyricsSpinnerPos); + return url; + } + + public static String addNewLines(String html) { + // Add new lines to the fetched text + Document document = Jsoup.parse(html); + // Makes html() preserve linebreaks and spacing + document.outputSettings(new Document.OutputSettings().prettyPrint(false)); + document.select("br").append("\\n"); + document.select("p").prepend("\\n\\n"); + String s = document.html().replaceAll("\\\\n", "\n"); + return Jsoup.clean(s, "", Whitelist.none(), new Document.OutputSettings().prettyPrint(false)); + } +} diff --git a/app/src/main/java/com/bernd32/jlyrics/utils/PreferencesManager.java b/app/src/main/java/com/bernd32/jlyrics/utils/PreferencesManager.java new file mode 100644 index 0000000..8828dda --- /dev/null +++ b/app/src/main/java/com/bernd32/jlyrics/utils/PreferencesManager.java @@ -0,0 +1,83 @@ +/* + * Copyright 2019 bernd32 + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.bernd32.jlyrics.utils; + +import android.content.Context; +import android.content.SharedPreferences; + +public class PreferencesManager { + +/* a thread-safe singleton class to make the global access method + synchronized, so that only one thread can execute this method at a time */ + + private static final String APP_PREFS = "com.bernd32.jlyrics.prefs"; + private static final String FONT_SIZE = "com.example.app.fontsize"; + private static final String DARK_THEME = "com.example.app.darktheme"; + + private static PreferencesManager sInstance; + private final SharedPreferences mPref; + + private PreferencesManager(Context context) { + mPref = context.getSharedPreferences(APP_PREFS, Context.MODE_PRIVATE); + } + + public static synchronized void initializeInstance(Context context) { + if (sInstance == null) { + sInstance = new PreferencesManager(context); + } + } + + public static synchronized PreferencesManager getInstance() { + if (sInstance == null) { + throw new IllegalStateException(PreferencesManager.class.getSimpleName() + + " is not initialized, call initializeInstance(..) method first."); + } + return sInstance; + } + + public void setFontSize(int value) { + mPref.edit() + .putInt(FONT_SIZE, value) + .apply(); + } + + public int getFontSize() { + return mPref.getInt(FONT_SIZE, 20); + } + + public void setDarkThemeSelected(boolean value) { + mPref.edit() + .putBoolean(DARK_THEME, value) + .apply(); + } + + public boolean getDarkThemeSelected() { + return mPref.getBoolean(DARK_THEME, false); + } + + public void remove(String key) { + mPref.edit() + .remove(key) + .apply(); + } + + public boolean clear() { + return mPref.edit() + .clear() + .commit(); + } +} diff --git a/app/src/main/res/drawable-hdpi/baseline_g_translate_white_24.png b/app/src/main/res/drawable-hdpi/baseline_g_translate_white_24.png new file mode 100644 index 0000000..53f866d Binary files /dev/null and b/app/src/main/res/drawable-hdpi/baseline_g_translate_white_24.png differ diff --git a/app/src/main/res/drawable-hdpi/baseline_search_white_24.png b/app/src/main/res/drawable-hdpi/baseline_search_white_24.png new file mode 100644 index 0000000..bf2c964 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/baseline_search_white_24.png differ diff --git a/app/src/main/res/drawable-hdpi/baseline_share_white_24.png b/app/src/main/res/drawable-hdpi/baseline_share_white_24.png new file mode 100644 index 0000000..6ead1d8 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/baseline_share_white_24.png differ diff --git a/app/src/main/res/drawable-mdpi/baseline_g_translate_white_24.png b/app/src/main/res/drawable-mdpi/baseline_g_translate_white_24.png new file mode 100644 index 0000000..cced92d Binary files /dev/null and b/app/src/main/res/drawable-mdpi/baseline_g_translate_white_24.png differ diff --git a/app/src/main/res/drawable-mdpi/baseline_search_white_24.png b/app/src/main/res/drawable-mdpi/baseline_search_white_24.png new file mode 100644 index 0000000..616e6df Binary files /dev/null and b/app/src/main/res/drawable-mdpi/baseline_search_white_24.png differ diff --git a/app/src/main/res/drawable-mdpi/baseline_share_white_24.png b/app/src/main/res/drawable-mdpi/baseline_share_white_24.png new file mode 100644 index 0000000..a4befbf Binary files /dev/null and b/app/src/main/res/drawable-mdpi/baseline_share_white_24.png differ diff --git a/app/src/main/res/drawable-xhdpi/baseline_g_translate_white_24.png b/app/src/main/res/drawable-xhdpi/baseline_g_translate_white_24.png new file mode 100644 index 0000000..4451b37 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/baseline_g_translate_white_24.png differ diff --git a/app/src/main/res/drawable-xhdpi/baseline_search_white_24.png b/app/src/main/res/drawable-xhdpi/baseline_search_white_24.png new file mode 100644 index 0000000..19b5362 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/baseline_search_white_24.png differ diff --git a/app/src/main/res/drawable-xhdpi/baseline_share_white_24.png b/app/src/main/res/drawable-xhdpi/baseline_share_white_24.png new file mode 100644 index 0000000..5864e38 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/baseline_share_white_24.png differ diff --git a/app/src/main/res/drawable-xxhdpi/baseline_g_translate_white_24.png b/app/src/main/res/drawable-xxhdpi/baseline_g_translate_white_24.png new file mode 100644 index 0000000..480539e Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/baseline_g_translate_white_24.png differ diff --git a/app/src/main/res/drawable-xxhdpi/baseline_search_white_24.png b/app/src/main/res/drawable-xxhdpi/baseline_search_white_24.png new file mode 100644 index 0000000..91e6d2b Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/baseline_search_white_24.png differ diff --git a/app/src/main/res/drawable-xxhdpi/baseline_share_white_24.png b/app/src/main/res/drawable-xxhdpi/baseline_share_white_24.png new file mode 100644 index 0000000..02cea48 Binary files /dev/null and b/app/src/main/res/drawable-xxhdpi/baseline_share_white_24.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/baseline_g_translate_white_24.png b/app/src/main/res/drawable-xxxhdpi/baseline_g_translate_white_24.png new file mode 100644 index 0000000..9b9378d Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/baseline_g_translate_white_24.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/baseline_search_white_24.png b/app/src/main/res/drawable-xxxhdpi/baseline_search_white_24.png new file mode 100644 index 0000000..c45a71b Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/baseline_search_white_24.png differ diff --git a/app/src/main/res/drawable-xxxhdpi/baseline_share_white_24.png b/app/src/main/res/drawable-xxxhdpi/baseline_share_white_24.png new file mode 100644 index 0000000..150f7d9 Binary files /dev/null and b/app/src/main/res/drawable-xxxhdpi/baseline_share_white_24.png differ diff --git a/app/src/main/res/drawable/favorite_border_black.xml b/app/src/main/res/drawable/favorite_border_black.xml new file mode 100644 index 0000000..254f1c8 --- /dev/null +++ b/app/src/main/res/drawable/favorite_border_black.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_cancel_black_24dp.xml b/app/src/main/res/drawable/ic_cancel_black_24dp.xml new file mode 100644 index 0000000..1c5d9f5 --- /dev/null +++ b/app/src/main/res/drawable/ic_cancel_black_24dp.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_filter_list_24dp.xml b/app/src/main/res/drawable/ic_filter_list_24dp.xml new file mode 100644 index 0000000..db3b74d --- /dev/null +++ b/app/src/main/res/drawable/ic_filter_list_24dp.xml @@ -0,0 +1,25 @@ + + + + + diff --git a/app/src/main/res/drawable/ic_keyboard_arrow_up_white_24dp.xml b/app/src/main/res/drawable/ic_keyboard_arrow_up_white_24dp.xml new file mode 100644 index 0000000..74f3b2c --- /dev/null +++ b/app/src/main/res/drawable/ic_keyboard_arrow_up_white_24dp.xml @@ -0,0 +1,21 @@ + + + + + diff --git a/app/src/main/res/layout-land/activity_main.xml b/app/src/main/res/layout-land/activity_main.xml new file mode 100644 index 0000000..1833af0 --- /dev/null +++ b/app/src/main/res/layout-land/activity_main.xml @@ -0,0 +1,182 @@ + + + + + + + + +