diff --git a/.idea/gradle.xml b/.idea/gradle.xml
index c5c0ff3b..8af96957 100644
--- a/.idea/gradle.xml
+++ b/.idea/gradle.xml
@@ -18,4 +18,4 @@
-
\ No newline at end of file
+
diff --git a/README.md b/README.md
index 4333e000..b215ff91 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
# QuickNovel
-Adfree FOSS Android app for downloading novels. It also functions as an Epub reader.
+Ad-free FOSS Android app for downloading novels. It also functions as an Epub reader.
**Discord:** https://discord.gg/5Hus6fM
@@ -45,7 +45,7 @@ Adfree FOSS Android app for downloading novels. It also functions as an Epub rea
- https://readnovelfull.com
-**Screenshoots:**
+**Screenshots:**
@@ -59,5 +59,5 @@ The app is purely for educational and personal use.
QuickNovel does not host any content on the app, and has no control over what media is put up or taken down. QuickNovel functions like any other search engine, such as Google. QuickNovel does not host, upload or manage any videos, films or content. It simply crawls, aggregates and displayes links in a convenient, user-friendly interface.
-It merely scrapes 3rd-party websites that are publicly accessable via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use QuickNovel at your own risk.
+It merely scrapes 3rd-party websites that are publicly accessible via any regular web browser. It is the responsibility of user to avoid any actions that might violate the laws governing his/her locality. Use QuickNovel at your own risk.
diff --git a/app/build.gradle b/app/build.gradle
index 25f5a056..4862fe61 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -93,6 +93,7 @@ dependencies {
implementation "io.noties.markwon:core:4.6.2"
implementation "io.noties.markwon:image-glide:4.6.2"
implementation "io.noties.markwon:html:4.6.2"
+ implementation "com.github.franmontiel:PersistentCookieJar:v1.0.1"
implementation "com.facebook.shimmer:shimmer:0.5.0"
}
diff --git a/app/src/main/java/com/lagradost/quicknovel/MainActivity.kt b/app/src/main/java/com/lagradost/quicknovel/MainActivity.kt
index 4253c6cf..48e65958 100644
--- a/app/src/main/java/com/lagradost/quicknovel/MainActivity.kt
+++ b/app/src/main/java/com/lagradost/quicknovel/MainActivity.kt
@@ -5,6 +5,7 @@ import android.content.Intent
import android.content.res.ColorStateList
import android.os.Bundle
import android.view.WindowManager
+import android.webkit.CookieManager
import androidx.activity.viewModels
import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
@@ -74,7 +75,7 @@ class MainActivity : AppCompatActivity() {
.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false).build()!!
private var _mainActivity: WeakReference? = null
- private var mainActivity
+ var mainActivity
get() = _mainActivity?.get()
private set(value) {
_mainActivity = WeakReference(value)
diff --git a/app/src/main/java/com/lagradost/quicknovel/providers/ArchiveOfOurOwnProvider.kt b/app/src/main/java/com/lagradost/quicknovel/providers/ArchiveOfOurOwnProvider.kt
new file mode 100644
index 00000000..5d5e56f2
--- /dev/null
+++ b/app/src/main/java/com/lagradost/quicknovel/providers/ArchiveOfOurOwnProvider.kt
@@ -0,0 +1,231 @@
+package com.lagradost.quicknovel.providers
+
+import androidx.preference.PreferenceManager
+import com.franmontiel.persistentcookiejar.PersistentCookieJar
+import com.franmontiel.persistentcookiejar.cache.SetCookieCache
+import com.franmontiel.persistentcookiejar.persistence.SharedPrefsCookiePersistor
+import com.lagradost.nicehttp.Requests
+import com.lagradost.quicknovel.*
+import com.lagradost.quicknovel.BaseApplication.Companion.context
+import com.lagradost.quicknovel.mvvm.debugAssert
+import com.lagradost.quicknovel.mvvm.debugException
+import com.lagradost.quicknovel.mvvm.debugWarning
+import okhttp3.FormBody
+import org.jsoup.Jsoup
+import org.jsoup.nodes.Element
+import kotlin.collections.ArrayList
+
+class ArchiveOfOurOwnProvider : MainAPI() {
+ private val cookieJar = PersistentCookieJar(SetCookieCache(), SharedPrefsCookiePersistor(context))
+ private val app = Requests(MainActivity.app.baseClient.newBuilder().cookieJar(cookieJar).build())
+ override val name = "Archive of Our Own"
+ override val mainUrl = "https://archiveofourown.org"
+
+ override val hasMainPage = true
+
+ override val rateLimitTime: Long = 50
+
+ override val iconId = R.drawable.ic_archive_of_our_own
+
+ override val iconBackgroundId = R.color.archiveOfOurOwnColor
+
+ override val orderBys = listOf(
+ Pair("Latest", "latest"),
+ )
+
+
+ override suspend fun loadMainPage(
+ page: Int,
+ mainCategory: String?,
+ orderBy: String?,
+ tag: String?,
+ ): HeadMainPageResponse {
+ tryLogIn()
+ val url =
+ "$mainUrl/works"
+ if (page > 1) return HeadMainPageResponse(
+ url,
+ ArrayList()
+ ) // Latest ONLY HAS 1 PAGE
+
+ val response = app.get(url)
+
+ val document = response.document
+ val works = document.select("li.work")
+ if (works.size <= 0) return HeadMainPageResponse(url, ArrayList())
+
+ val returnValue: ArrayList = ArrayList()
+ for (h in works) {
+ val workLink = h?.selectFirst("div.header.module > h4.heading > a")
+ if (workLink == null){
+ debugWarning { "Ao3 work has no actual work?" }
+ continue
+ }
+ val name = workLink.text()
+ val url = workLink.attr("href")
+
+ returnValue.add(
+ SearchResponse(
+ name,
+ fixUrl(url),
+ apiName = this.name
+ )
+ )
+ }
+ return HeadMainPageResponse(url, returnValue)
+ }
+
+
+ override suspend fun search(query: String): List {
+ tryLogIn()
+ val response = app.get("$mainUrl/works/search?work_search[query]=$query")
+
+ val document = response.document
+ val works = document.select("li.work")
+ if (works.size <= 0) return ArrayList()
+ val returnValue: ArrayList = ArrayList()
+ for (h in works) {
+ val workLink = h?.selectFirst("div.header.module > h4.heading > a")
+ if (workLink == null){
+ debugWarning { "Ao3 work has no actual work?" }
+ continue
+ }
+ val name = workLink.text()
+ val url = workLink.attr("href")
+
+ val authorLink = h.selectFirst("div.header.module > h4.heading > a[rel=\"author\"]")
+ if (authorLink == null){
+ debugWarning { "Ao3 work has no actual author?" }
+ continue
+ }
+ val author = authorLink.attr("href")
+
+ returnValue.add(
+ SearchResponse(
+ name,
+ fixUrl(url),
+ fixUrlNull(author),
+ apiName = this.name
+ )
+ )
+ }
+ return returnValue
+ }
+
+ override suspend fun load(url: String): LoadResponse? {
+ tryLogIn()
+ val response = app.get("$url/?view_adult=true")
+
+ val document = response.document
+
+ val name = document.selectFirst("h2.title.heading")?.text().toString()
+ val author = document.selectFirst("h3.byline.heading > a[rel=\"author\"]")
+ val peopleVoted = document.selectFirst("dd.kudos")?.text()?.replace(",","")?.toInt()
+ val views = document.selectFirst("dd.hits")?.text()?.replace(",","")?.toInt()
+ val synopsis = document.selectFirst("div.summary.module > blockquote.userstuff")
+ ?.children()?.joinToString("\n", transform = Element::text)
+
+ val tags = document.select("a.tag").map(org.jsoup.nodes.Element::text)
+
+ val chaptersResponse = app.get("$url/navigate?view_adult=true")
+ val chapterDocument = Jsoup.parse(chaptersResponse.text)
+
+ val chapters = chapterDocument.selectFirst("ol.chapter.index.group[role=\"navigation\"]")
+
+ val data = chapters?.children()?.map {
+ val link = it.child(0)
+ val date = it.child(1)
+ ChapterData(
+ name = link.text().toString(),
+ url = fixUrl(link.attr("href").toString()),
+ dateOfRelease = date.text()
+ )
+ }
+
+
+ return StreamResponse(
+ url,
+ name,
+ data ?: ArrayList(),
+ author = author?.text(),
+ peopleVoted.toString(),
+ views,
+ synopsis = synopsis,
+ tags = tags
+ )
+ }
+
+ override suspend fun loadHtml(url: String): String? {
+ tryLogIn()
+ val response = app.get(url)
+ val document = Jsoup.parse(response.text)
+ return document.selectFirst("div.chapter")?.html()
+ }
+
+ private var hasLoggedIn = false
+ private suspend fun tryLogIn() {
+
+ if (hasLoggedIn) return
+
+ //Don't try to log in if we don't have email and password
+ val preferenceManager = PreferenceManager.getDefaultSharedPreferences(context!!.applicationContext)
+
+ val ao3Email = preferenceManager.getString(context?.getString(R.string.ao3_email_key),"")!!
+ val ao3Password = preferenceManager.getString(context?.getString(R.string.ao3_password_key),"")!!
+
+ if (ao3Email == "" || ao3Password == ""){
+ return
+ }
+
+ val response = app.get("$mainUrl/works/new", allowRedirects = false)
+ if(response.code == 200){
+ hasLoggedIn = true
+ return
+ }
+ if(response.code != 302) {
+ debugException { "AO3 isn't redirecting us to login page for some reason. If issue persists please report to extension creator." }
+ return
+ }
+
+ val loginPageResponse = app.get("$mainUrl/users/login")
+
+ val authenticityToken =
+ loginPageResponse.document.selectFirst("form.new_user#new_user")
+ ?.selectFirst("input[type=hidden][name=authenticity_token]")
+ ?.attr("value")!!
+
+
+ val loginResponse = app.post("$mainUrl/users/login", requestBody = FormBody.Builder()
+ .add("user[login]", ao3Email)
+ .add("user[password]", ao3Password)
+ .add("user[remember_me]", "1")
+ .add("commit", "Log in")
+ .add("authenticity_token", authenticityToken)
+ .build()
+ )
+
+ /*data= mapOf(
+ Pair("user[login]", ao3Email),
+ Pair("user[password]", ao3Password),
+ Pair("user[remember_me]", "1"),
+ Pair("commit", "Log in"),
+ Pair("authenticity_token", authenticityToken),
+ )*/
+
+ if(loginResponse.okhttpResponse.priorResponse == null){
+ if(loginResponse.text.contains("The password or user name you entered doesn't match our records.")){
+ if (!preferenceManager.edit().putString(context?.getString(R.string.ao3_password_key), "").commit()){
+ debugException { "Something went wrong clearing your password!" }
+ }
+ debugWarning { "Username or Password incorrect! Password's been cleared" }
+ }else{
+ debugException { "Something went wrong logging you in." }
+ }
+
+ }else{
+ debugAssert( {response.url.startsWith("$mainUrl/users") && response.url != "$mainUrl/users/login"},
+ {"Expected to be sent to $mainUrl/users/yourusername was instead sent to ${response.url}"})
+ }
+
+ }
+}
diff --git a/app/src/main/java/com/lagradost/quicknovel/ui/settings/ProviderSettingsFragment.kt b/app/src/main/java/com/lagradost/quicknovel/ui/settings/ProviderSettingsFragment.kt
new file mode 100644
index 00000000..f60b9935
--- /dev/null
+++ b/app/src/main/java/com/lagradost/quicknovel/ui/settings/ProviderSettingsFragment.kt
@@ -0,0 +1,13 @@
+package com.lagradost.quicknovel.ui.settings
+
+
+import android.os.Bundle
+import androidx.preference.*
+import com.lagradost.quicknovel.R
+
+class ProviderSettingsFragment : PreferenceFragmentCompat() {
+
+ override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
+ setPreferencesFromResource(R.xml.provider_settings, rootKey)
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/com/lagradost/quicknovel/util/Apis.kt b/app/src/main/java/com/lagradost/quicknovel/util/Apis.kt
index bda9c8eb..47aadb03 100644
--- a/app/src/main/java/com/lagradost/quicknovel/util/Apis.kt
+++ b/app/src/main/java/com/lagradost/quicknovel/util/Apis.kt
@@ -50,6 +50,7 @@ class Apis {
IndoWebNovelProvider(),
SakuraNovelProvider(),
WattpadProvider(),
+ ArchiveOfOurOwnProvider(),
)
fun getApiFromName(name: String): APIRepository {
diff --git a/app/src/main/java/com/lagradost/quicknovel/util/SingleSelectionHelper.kt b/app/src/main/java/com/lagradost/quicknovel/util/SingleSelectionHelper.kt
index 50ffec7a..9b2ddc52 100644
--- a/app/src/main/java/com/lagradost/quicknovel/util/SingleSelectionHelper.kt
+++ b/app/src/main/java/com/lagradost/quicknovel/util/SingleSelectionHelper.kt
@@ -13,7 +13,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
import com.lagradost.quicknovel.R
object SingleSelectionHelper {
- fun Context.showDialog(
+ private fun Context.showDialog(
dialog: Dialog,
items: List,
selectedIndex: List,
@@ -110,6 +110,7 @@ object SingleSelectionHelper {
)
}
+
fun Context.showDialog(
items: List,
selectedIndex: Int,
@@ -160,4 +161,5 @@ object SingleSelectionHelper {
dismissCallback
)
}
+
}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_archive_of_our_own.xml b/app/src/main/res/drawable/ic_archive_of_our_own.xml
new file mode 100644
index 00000000..8f6b844e
--- /dev/null
+++ b/app/src/main/res/drawable/ic_archive_of_our_own.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index 5fddab9b..9499b929 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -8,6 +8,7 @@
#FFF
#24242B
+ @color/primaryGrayBackground
@color/primaryGrayBackground
#1670bf
#FFF
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 2a926219..a8b61303 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -8,6 +8,9 @@
provider_lang_key
restore_key
+ ao3_email
+ ao3_password
+
Restore data from backup
Search Novels
@@ -126,6 +129,7 @@
Hide
Reload Chapter
Provider Languages
+ Provider Settings
Apply
Cancel
Keep screen active
diff --git a/app/src/main/res/xml/provider_settings.xml b/app/src/main/res/xml/provider_settings.xml
new file mode 100644
index 00000000..8d57934d
--- /dev/null
+++ b/app/src/main/res/xml/provider_settings.xml
@@ -0,0 +1,20 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/settings.xml b/app/src/main/res/xml/settings.xml
index beaf7f77..54680420 100644
--- a/app/src/main/res/xml/settings.xml
+++ b/app/src/main/res/xml/settings.xml
@@ -29,7 +29,13 @@
+ android:title="@string/provider_lang_settings">
+
+
+
@@ -100,6 +106,8 @@
android:icon="@drawable/baseline_restore_page_24"
android:key="@string/restore_key"
android:title="@string/restore_settings" />
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file